Почему блок использования не может безопасно инициализировать переменную?
Почему это дает ошибку компиляции?
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 хочет сохранить семантическую согласованность для всех функций.