【问题标题】:Why can a use block not safely initialize a var?为什么 use 块不能安全地初始化 var?
【发布时间】:2017-12-27 13:40:12
【问题描述】:

为什么会出现编译错误?

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 的精神。

【问题讨论】:

    标签: kotlin autocloseable


    【解决方案1】:

    由于use { ... } 不是语言结构而是is just a library function,编译器不知道(并且目前不努力证明)您传递的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 返回的值(这是您的块返回的值)来初始化您的变量:

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

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

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

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

    【讨论】:

    • 如果它很懒,为什么这段代码不能工作? var myVar: MyType ;val b = { println(myVar) }; myVar = autoClosable.foo(),否则就是我的反对票。 :)
    • @holi-java,为什么它应该起作用?编译器禁止val b = { println(myVar) }(可以在分配myVar 之前调用它——myVar 在创建b 的那一刻之前被统一化)比检查它要容易得多在b 的每个调用站点上,myVar 已经被初始化。
    • @holi-java,如果这不是您要问的,那我一定是误会了您,您能否重新提出问题?
    • 嗨,你让我把它作为问题发布?
    • 但它应该用ObjectRef初始化myVar,因为正如你所说的lambda是惰性的,编译器不能确定lambda被调用。
    【解决方案2】:

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

    这就是为什么编译器不会让你做你想做的事。

    【讨论】:

    • 实际上,use 会重新抛出它捕获的异常。如果在use { ... } 块内抛出异常,则会在use 调用之外抛出异常,并且根本不会到达println(myVar) 行。 完美的编译器 不会禁止此代码,因为它会猜测use 只调用该块一次并将异常重新抛出到调用代码中。 :) Kotlin 编译器只是对 lambda 执行 exactly-once 分析。
    • 哦,对了,我没有检查。谢谢你的解释!
    【解决方案3】:

    这是因为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 不允许您将变量更改出令人讨厌的类范围。事实上,myVar实际上是最终的。例如:

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

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

    如果抓到什么,代码也编译不出来,例如:

    //  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 将来是否会在另一个内联函数的 lambda 主体中使用。或者 kotlin 想要保持所有函数的语义一致。

    【讨论】:

    • usetry 的区别在于try 是一种具有已知语义的语言结构:它将执行主体,如果抛出异常,则执行对应的catch 块。因此,编译器可以证明myVar 是通过各种方式分配的。而use 则完全不同:它只是一个库函数,它可以管理以任意方式传递的 lambda(例如多次调用或从不调用)。因此编译器不能确定代码块是否被执行过,它保守地禁止可能不正确的代码。
    • 好吧,但是 lambda 主体的代码被内联到调用站点函数中,所以应该是正确的。我也测试过,嵌套的try-catch 也可以工作。但内联函数不能。例如:try { try{ TODO() }catch(e:Throwable){throw e} } catch (e: Throwable) { myVar=autoClosable.foo() }
    • 那么为什么内联函数不能工作,我唯一能想到的是编译器不确定ObjectRefelement 是否已初始化。我在中间的回答中描述了它。也许你永远看不到它。
    • 即使 lambda 是内联的,但当前并未在其中分析控制流。它只是没有实现,编译器只是禁止这段代码,就好像没有调用 lambda 一样。为了使其工作,编译器应该分析内联函数体以证明它调用了 lambda 或生成的字节码。目前两者都没有完成。
    • 另外,当 var 仅用于内联 lambda 时,它根本不会包装到 ObjectRef 中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-28
    相关资源
    最近更新 更多