Почему блок использования не может безопасно инициализировать переменную?

Почему это дает ошибку компиляции?

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 с какой-то фиктивной ценностью, которая совсем не в духе Котлина.

3 ответа

Решение

Поскольку use { ... } это не языковая конструкция, а просто библиотечная функция, компилятор не знает (и в настоящее время не прилагает усилий, чтобы доказать), что лямбда, которую вы передаете, когда-либо выполняется. Поэтому использование переменной, которая не может быть инициализирована, запрещено.

Например, сравните ваш код с этим вызовом функции. Без дополнительного анализа кода они идентичны для компилятора:

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 как выражение:

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

Теоретически, встроенные функции могут быть проверены, чтобы вызывать лямбду ровно один раз, и если компилятор Kotlin может это сделать, это позволит использовать ваш вариант использования и некоторые другие. Но это еще не реализовано.

В случае возникновения исключения при выполнении it.foo(), use блок поймает исключение, закройте autoClosable, а затем вернуться. В этом случае, myVar останется неинициализированным.

Вот почему компилятор не позволит вам делать то, что вы пытаетесь сделать.

Это потому что use является встроенной функцией, которая означает, что лямбда-тело будет встроено в функцию call-site, и фактический тип переменной myVar зависит от его контекста.

ЕСЛИ myVar используется в лямбде для чтения, тип MyType или его супертип. например:

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

autoClosable.use {
    myVar.todo()
}

ЕСЛИ myVar используется в лямбде для записи, фактический тип 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 используется ли в лямбда-теле другой недействующей функции в будущем. или kotlin хочет сохранить семантическую согласованность для всех функций.

Другие вопросы по тегам