kotlin - 非直观类型推断的 Kotlin 案例
问题描述
我发现了一些类型推断的非直觉行为。因此,语义等效代码的工作方式不同,具体取决于编译器推断出的有关函数返回类型的信息。当您在最小单元测试中重现此案例时,或多或少清楚发生了什么。但我担心在编写框架代码时,这种行为可能会很危险。
下面的代码说明了这个问题,我的问题是:
为什么无条件的
puzzler1
调用notok1
会抛出 NPE?据我从字节码了解,调用ACONST_NULL ATHROW
后立即抛出 NPEpuzzler1
,忽略返回值。<T : TestData>
编译器推断类型时忽略上限()是否正常?suspend
如果您在函数中添加修饰符,NPE 变成 ClassCastException 是否是一个错误?当然,我知道runBlocking+suspend
call 给了我们不同的字节码,但是“coroutineized”代码不应该尽可能地等同于传统代码吗?有没有办法以
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")
?好吧,因为它从不返回值,所以你可以在任何你喜欢的地方使用它,不管期望什么类型的对象,它总是会进行类型检查。我们说它有 type Nothing
。这种类型没有值,它是每种类型的子类型。因此,在 中notok1
,您可以调用puzzler1<Nothing>
。从构造TestData
到T = Nothing
内部的转换puzzler1<Nothing>
是不健全但未经检查的,并且puzzler1
当它的类型签名表明它不应该能够时最终返回。notok1
注意到puzzler1
当它说它不能返回时已经返回,并立即抛出异常。它的描述性不是很好,但我相信它抛出 NPE 的原因是因为如果一个无法返回的函数返回了,就会出现“严重错误”,所以语言决定程序应该尽快死掉。
对于notok2
,您实际上确实得到T = TestData
了: return 的一个分支,另一个,以及其中的if
LUB是(因为是 的子类型)。没有理由相信不能回来,所以它不设陷阱一回来就死。Nothing
TestData
TestData
Nothing
TestData
notok2
puzzler1<TestData>
puzzler1
notok3
与 有本质上相同的问题notok1
。返回类型 ,Nothing
意味着唯一要做的puzzler2<Nothing>
就是抛出一个异常。因此,协程处理代码notok3
期望协程保存 aThrowable
并包含重新抛出它的代码,但不包含处理实际返回值的代码。当puzzler2
实际返回时,notok3
尝试将其TestData
转换为 aThrowable
并失败。notok4
出于同样的原因notok2
。
解决这个烂摊子的办法就是不使用不健全的演员表。有时 puzzler1<T>
/puzzler2<T>
将能够返回 a T
,如果传递的函数实际上返回 a T
。但是,如果该函数抛出,它们只能返回 a TestData
,并且 aTestData
不是a (a是 a ,而不是相反)。(和类似的)的正确签名是T
T
TestData
puzzler1
puzzler2
fun <T : TestData> puzzler1(resultWrapper: (String) -> T): TestData
由于函数在返回类型中是协变的,因此您可以摆脱类型参数
fun puzzler1(resultWrapper: (String) -> TestData): TestData
推荐阅读
- django - 从 Django 中删除模板
- nativescript - Nativescript 错误:执行 webpack 失败,退出代码为 1
- javascript - 发出 GET 请求时向 axios 添加标头字段后的 CORS 问题
- javascript - 类组件不是每次都收到道具
- javascript - 如何在页面上绘制多个(动态生成的)图表(使用 chartjs)但一次只显示一个
- git - 为什么在我的迁移中通过 VS 代码中的终端创建分支时出现错误
- sql - 在同一个表中找到重复的行并在sql中标记它们
- php - 未找到基表或视图:1146 表“nextcloud.oc_appconfig”不存在
- reactjs - 如何将下拉表单上的标签从顶部移动到侧面
- html - 如何在不影响页面上任何其他内容的情况下使页面的背景图像变得模糊?