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

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

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

Может быть, компилятор просто видит { myVar= it.foo() } как функцию, которая передается другой функции, и не знает, когда и даже будет ли она выполнена?

Но поскольку use — это не просто функция, а замена Kotlin функции try-with-resource в Java, некоторые специальные знания о ней были бы уместны, не так ли? Прямо сейчас я вынужден инициализировать myVar каким-то фиктивным значением, что совсем не в духе Котлина.


person Marco Eckstein    schedule 21.07.2017    source источник


Ответы (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 мог это сделать, он разрешил бы ваш вариант использования и некоторые другие. Но это еще не реализовано.

person hotkey    schedule 21.07.2017
comment
если лень, почему этот код не работает? var myVar: MyType ;val b = { println(myVar) }; myVar = autoClosable.foo(), В противном случае мой голос против. :) - person holi-java; 21.07.2017
comment
@holi-java, а почему это должно работать? Компилятору намного проще запретить val b = { println(myVar) } (который может вызываться до присвоения myVar -- myVar унитализируется до момента создания b), чем проверять, что на каждом участке вызова b myVar уже инициализирован. - person hotkey; 21.07.2017
comment
@holi-java, если вы спрашивали не об этом, то я, должно быть, неправильно вас понял, не могли бы вы переформулировать вопрос? - person hotkey; 21.07.2017
comment
Привет, вы просите меня опубликовать это как вопрос? - person holi-java; 21.07.2017
comment
но он должен быть инициализирован myVar с ObjectRef, поскольку, как вы сказали, лямбда ленивая, компилятор не может быть уверен, что лямбда вызывается. - person holi-java; 21.07.2017
comment
мы знаем любой язык, такой как javascript, функция может получить доступ к переменной за пределами своей области видимости и может быть инициализирована позже. но в java, если вы хотите, чтобы и записать значение из области лямбда, и прочитать значение в области лямбда, мы должны использовать ObjectRef. так как переменная вне лямбда-области действия является эффективной-финальной. и я также вижу байт-код kotlin, что тип точно ObjectRef. почему нельзя компилировать? - person holi-java; 21.07.2017
comment
@holi-java, ну, это просто то, как это работает под капотом, но посмотрите на семантику кода. Когда вы пишете println(myVar), вы ожидаете, что будет напечатано значение MyType, а не ObjectRef, созданное компилятором для его хранения. На самом деле, компилятор делает все, чтобы переменная, обернутая в ObjectRef, выглядела как обычная var, а ObjectRef никогда не показывается пользователю (если только он не заглянет в байт-код). Таким образом, говоря, что myVar остается неинициализированным, я имею в виду, что либо локальная переменная, либо оболочка ObjectRef не содержат значение, которое должно быть присвоено. - person hotkey; 21.07.2017
comment
На самом деле, в своем ответе я отвлекаюсь от ObjectRef, который полностью является деталью реализации и не требует объяснения семантики кода — мы можем просто думать о var как о контейнере для переменной, независимо от того, обернута ли она или нет, компилятор гарантирует, что мы присвоим значение перед использованием var. - person hotkey; 21.07.2017
comment
привет, первый раз тоже думаю лениво. но когда я выгружаю код, я обнаружил, что не могу сказать, что это лениво правильно. Я думаю, вы должны дать исчерпывающий ответ. :) - person holi-java; 21.07.2017
comment
вызвать лямбду ровно один раз. так что не так с моим ответом, сэр? - person holi-java; 21.07.2017
comment
Что вы подразумеваете под ленивым? Фактическая причина ограничения, с которым столкнулся OP, заключается в том, что компилятор не доказывает, что лямбда когда-либо вызывалась. Как только в компиляторе будет реализована проверка exactly-once, о которой я упоминал, код в вопросе будет разрешен. - person hotkey; 21.07.2017
comment
Мне жаль, что я не силен в английском. Я сказал, что также обнаружил, что лямбда ленивая, но когда я выгружаю байт-код, я обнаружил, что не могу сказать, что это лениво правильно. поскольку байт-код тела лямбды анализируется компилятором. ленивый Я имею в виду, что код в теле лямбды никогда не вызывается, если лямбда не вызывается. - person holi-java; 21.07.2017
comment
сэр, из-за моего плохого английского я создал его как вопрос. Я рад, что вы идете, чтобы помочь мне решить мое замешательство. во-первых, спасибо. - person holi-java; 21.07.2017
comment
Относительно исходного ответа: Отлично! Написав это таким образом, вы даже можете опустить явное объявление типа MyType. Возможно, вы захотите отредактировать свой ответ. - person Marco Eckstein; 28.07.2017

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

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

person zsmb13    schedule 21.07.2017
comment
На самом деле, use повторно выдает перехваченное исключение. Если внутри блока use { ... } выбрасывается исключение, оно выбрасывается вне вызова use, а строка println(myVar) вообще не достигается. идеальный компилятор не стал бы запрещать этот код, потому что он догадался бы, что use вызывает блок ровно один раз, и возвращает исключение обратно в вызывающий код. :) Компилятор Kotlin просто выполняет анализ ровно один раз для лямбда-выражений. - person hotkey; 21.07.2017
comment
А, точно, я не проверял. Спасибо за объяснение! - person zsmb13; 21.07.2017

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

person holi-java    schedule 21.07.2017
comment
Разница между use и try в том, что try — это языковая конструкция с известной семантикой: она выполнит тело, а если возникнет исключение, то выполнит соответствующий блок catch. Таким образом, компилятор может во что бы то ни стало доказать, что myVar присваивается. А use — совсем другое дело: это просто библиотечная функция, и она может управлять лямбдой, которую она передает произвольным образом (например, вызывать ее несколько раз или никогда не вызывать). Поэтому компилятор не может быть уверен, что блок кода когда-либо будет выполнен, и он консервативно запрещает возможный неправильный код. - person hotkey; 21.07.2017
comment
хорошо, но код тела лямбды встроен в функцию call-site, так что это должно быть правильно. что у меня также есть тест, вложенный try-catch также может работать. но встроенная функция не может. например: try { try{ TODO() }catch(e:Throwable){throw e} } catch (e: Throwable) { myVar=autoClosable.foo() } - person holi-java; 21.07.2017
comment
так почему встроенная функция не может работать, единственное, что я могу думать, это то, что компилятор не уверен, что element из ObjectRef инициализирован. и я описываю это в ответе в середине. может быть, вы никогда этого не увидите. - person holi-java; 21.07.2017
comment
Несмотря на то, что лямбда встроена, поток управления в ней в данный момент не анализируется. Это просто не реализовано, и компилятор просто запрещает этот код, как будто лямбда не вызывается. Чтобы заставить его работать, компилятор должен проанализировать либо тело встроенной функции, чтобы доказать, что она вызывает лямбду, либо результирующий байт-код. Ни то, ни другое не делается в данный момент. - person hotkey; 21.07.2017
comment
Кроме того, когда var используется только во встроенных лямбда-выражениях, он вообще не упаковывается в ObjectRef. - person hotkey; 21.07.2017
comment
Не могли бы вы дать исходный код или ссылку, чтобы я мог его увидеть? Я знаю, что вы внесли свой вклад в Kotlin. но твой ответ не убедил меня с первого раза. - person holi-java; 21.07.2017
comment
Хорошо, конечно, я сделаю это чуть позже. - person hotkey; 21.07.2017