为什么 use 块不能安全地初始化 var?

Why can a use block not safely initialize a var?

为什么会出现编译错误?

val autoClosable = MyAutoClosable()
var myVar: MyType
autoClosable.use {
    myVar= it.foo()
}
println(myVar) // Error: Variable 'myVar' must be initialized

也许编译器只是将 { myVar= it.foo() } 视为传递给另一个函数的函数,而不知道它何时或什至是否会执行?

但由于 use 不仅仅是一个函数,而且是 Kotlin 对 Java 的 try-with-resource 的替代,因此了解一些关于它的特殊知识是合适的,不是吗?现在,我被迫用一些虚拟值初始化 myVar,这根本不符合 Kotlin 的精神。

如果在执行it.foo()时发生异常,use块将捕获异常,关闭您的autoClosable,然后return。在这种情况下,myVar 将保持未初始化状态。

这就是为什么编译器不允许您做您想做的事情的原因。

这是因为use是一个内联函数,这意味着lambda主体将被内联到调用点函数,而变量的实际类型myVar 取决于其上下文。

IF myVar 在 lambda 中用于读取,类型是 MyType 或其超类型。例如:

//      v--- the actual type here is MyType
var myVar: MyType = TODO()

autoClosable.use {
    myVar.todo()
}

IF myVar在lambda中是用来写的,实际类型是一个ObjectRef。为什么?这是因为 Java 不允许您将变量更改出烦人的 class 范围。事实上,myVareffectively-final。例如:

//  v--- the actual type here is an ObjectRef type.
var myVar: MyType

autoClosable.use {
    myVar = autoClosable.foo()
}

所以 当编译器检查println(myVar) 时,它不能确定ObjectRef 的元素是否被初始化。然后引发编译器错误。

IF你抓到什么了,代码也无法编译,例如:

//  v--- the actual type here is an ObjectRef type.
var myVar: MyType
try {
    autoClosable.use {
        myVar = it.foo()
    }
} catch(e: Throwable) {
    myVar = MyType()
}

//       v--- Error: Variable 'myVar' must be initialized
println(myVar) 

但是myVar 的实际类型是 MyType 时,它工作正常。例如:

var myVar: MyType
try {
    TODO()
} catch(e: Throwable) {
    myVar = MyType()
}

println(myVar) // works fine

为什么 kotlin没有优化内联函数直接使用MyType写?

我唯一认为的是,编译器不知道 myVar 将来是否在另一个 uninline 函数的 lambda 主体中使用。或者 kotlin 想要保持所有功能的语义一致。

由于 use { ... } 不是语言构造而是 [​​=16=],编译器不知道(目前,也不会努力证明)您传递的 lambda 是 曾经执行过。因此禁止使用可能未初始化的变量。

例如,将您的代码与此函数调用进行比较。没有额外的代码分析,它们对于编译器是相同的:

inline fun ignoreBlock(block: () -> Unit) = Unit

var myVar: MyType
ignoreBlock { myVar = it.foo() }
println(myVar) // Expectedly, `myVar` stays uninitialized, and the compiler prohibits it

要绕过此限制,您可以使用从 use 返回的值(这是您的块 returns 的值)来初始化您的变量:

val myVar = autoClosable.use {
    it.foo()
}

如果你还想处理它可能抛出的异常,那么使用try as an expression

val myVar = try {
    autoClosable.use {
        it.foo()
    }
} catch (e: SomeException) {
    otherValue   
}

理论上,实际上可以检查内联函数以仅调用一次 lambda,如果 Kotlin 编译器可以做到这一点,它将允许您的用例和其他一些用例。但这尚未实施。