gpt4 book ai didi

非直觉类型推断的 Kotlin 案例

转载 作者:行者123 更新时间:2023-12-01 12:02:17 27 4
gpt4 key购买 nike

我发现了一些类型推断的非直觉行为。因此,语义等效代码的工作方式不同,具体取决于编译器推断出的有关函数返回类型的信息。当您在最小单元测试中重现此案例时,或多或少会清楚发生了什么。但我担心在编写框架代码时,这种行为可能很危险。

下面的代码说明了问题,我的问题是:

  1. 为什么 puzzler1来自 notok1 的电话无条件抛出NPE?据我从字节码了解,ACONST_NULL ATHROWpuzzler1 之后立即抛出 NPE调用,忽略返回值。

  2. 编译器推断类型时忽略上限(<T : TestData>)是否正常?

  3. 添加suspend NPE变成ClassCastException是不是bug函数修饰符?当然,我明白runBlocking+suspend call 给了我们不同的字节码,但“协程化”代码不应该尽可能等同于常规代码吗?

  4. 有没有办法重写puzzler1以某种方式编码,消除不清晰?

@Suppress("UnnecessaryVariable", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST", "RedundantSuspendModifier")
class PuzzlerTest {

open class TestData(val value: String)

lateinit var whiteboxResult: TestData

fun <T : TestData> puzzler1(
resultWrapper: (String) -> T
): T {
val result = try {
resultWrapper("hello")
} catch (t: Throwable) {
TestData(t.message!!) as T
}
whiteboxResult = result
return result // will always return TestData type
}

// When the type of `puzzler1` is inferred to TestData, the code works as expected:
@Test
fun ok() {
val a = puzzler1 { TestData("$it world") }
// the same result inside `puzzler1` and outside of it:
assertEquals("hello world", whiteboxResult.value)
assertEquals("hello world", a.value)
}

// But when the type of `puzzler1` is not inferred to TestData, the result is rather unexpected.
// And compiler ignores the upper bound <T : TestData>:
@Test
fun notok1() {
val a = try {
puzzler1 { throw RuntimeException("goodbye") }
} catch (t: Throwable) {
t
}
assertEquals("goodbye", whiteboxResult.value)
assertTrue(a is NullPointerException) // this is strange
}

// The same code as above, but with enough information for the compiler to infer the type:
@Test
fun notok2() {
val a = puzzler1 {
@Suppress("ConstantConditionIf")
if (true)
throw RuntimeException("goodbye")
else {
// the type is inferred from here
TestData("unreachable")

// The same result if we write:
// puzzler1<TestData> { throw RuntimeException("goodbye") }
}
}

assertEquals("goodbye", whiteboxResult.value)
assertEquals("goodbye", (a as? TestData)?.value) // this is stranger
}

// Now create the `puzzler2` which only difference from `puzzler1` is `suspend` modifier:

suspend fun <T : TestData> puzzler2(
resultWrapper: (String) -> T
): T {
val result = try {
resultWrapper("hello")
} catch (t: Throwable) {
TestData(t.message!!) as T
}
whiteboxResult = result
return result
}

// Do exactly the same test as `notok1` and NullPointerException magically becomes ClassCastException:
@Test
fun notok3() = runBlocking {
val a = try {
puzzler2 { throw RuntimeException("goodbye") }
} catch (t: Throwable) {
t
}
assertEquals("goodbye", whiteboxResult.value)
assertTrue(a is ClassCastException) // change to coroutines and NullPointerException becomes ClassCastException
}

// The "fix" is the same as `notok2` by providing the compiler with info to infer `puzzler2` return type:
@Test
fun notok4() = runBlocking {
val a = try {
puzzler2<TestData> { throw RuntimeException("goodbye") }

// The same result if we write:
// puzzler2 {
// @Suppress("ConstantConditionIf")
// if (true)
// throw RuntimeException("goodbye")
// else
// TestData("unreachable")
// }
} catch (t: Throwable) {
t
}
assertEquals("goodbye", whiteboxResult.value)
assertEquals("goodbye", (a as? TestData)?.value)
}
}

最佳答案

throw RuntimeException("goodbye") 的类型是什么? ?好吧,因为它从不返回值,你可以在任何你喜欢的地方使用它,无论期望什么类型的对象,它总是会进行类型检查。我们说它的类型是 Nothing .这种类型没有值,它是所有类型的子类型。因此,在 notok1 , 你有一个调用 puzzler1<Nothing> .来自构建的 Actor TestDataT = Nothing里面puzzler1<Nothing>不健全但未经检查,puzzler1当它的类型签名说它不应该能够时最终返回。 notok1注意到 puzzler1当它说它不能时已经返回,并立即抛出异常。它的描述性不强,但我相信它抛出 NPE 的原因是因为如果无法返回的函数已经返回,则出现了“严重错误”,因此语言决定程序应该尽快结束。

对于 notok2 ,你实际上得到了 T = TestData :if的一个分支返回 Nothing , 其他 TestData ,其中的 LUB 是 TestData (因为 NothingTestData 的子类型)。 notok2没有理由相信puzzler1<TestData>无法返回,所以它不会设置陷阱尽快死亡 puzzler1返回。

notok3notok1 本质上有相同的问题.返回类型,Nothing , 意味着唯一的 puzzler2<Nothing>会做的是抛出异常。 notok3中的协程处理代码因此期望协程持有一个 Throwable并包含重新抛出它的代码,但不包含处理实际返回值的代码。当puzzler2实际上确实返回,notok3试图施放 TestData进入 Throwable并失败了。 notok4出于同样的原因工作 notok2

这个困惑的解决方案就是不使用不合理的转换。 有时 puzzler1<T>/puzzler2<T>将能够返回 T ,如果传递的函数实际上返回一个 T .但是,如果该函数抛出,它们只能返回 TestData。 , 和一个 TestData 不是 T (一个 T 是一个 TestData ,而不是相反)。 puzzler1 的正确签名(对于 puzzler2 也是如此)是

fun <T : TestData> puzzler1(resultWrapper: (String) -> T): TestData

因为函数在返回类型上是协变的,所以你可以去掉类型参数

fun puzzler1(resultWrapper: (String) -> TestData): TestData

关于非直觉类型推断的 Kotlin 案例,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60760071/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com