Ошибка компиляции: Smart cast to '<type>' невозможен, потому что '<variable>' - локальная переменная, которая захватывается изменяющимся закрытием
Чтобы упростить мой реальный прецедент, предположим, что я хочу найти максимальное число в списке:
var max : Int? = null
listOf(1, 2, 3).forEach {
if (max == null || it > max) {
max = it
}
}
Однако компиляция не выполняется со следующей ошибкой:
Смарт-литье в "Int" невозможно, потому что "max" - это локальная переменная, которая захватывается изменяющимся закрытием
Почему переменная закрытие предотвращает работу смарт-броска в этом примере?
Ответы
Ответ 1
В общем случае, когда изменяемая переменная фиксируется при закрытии лямбда-функции, интеллектуальные приведения не применимы к этой переменной как внутри лямбды, так и в области объявления после создания лямбда.
Это из-за того, что функция может выйти из своей охватывающей области и может быть выполнена позже в другом контексте, возможно, несколько раз и, возможно, параллельно. В качестве примера рассмотрим гипотетическую функцию List.forEachInParallel { ... }
, которая выполняет заданную лямбда-функцию для каждого элемента списка, но параллельно.
Компилятор должен сгенерировать код, который будет оставаться верным даже в этом серьезном случае, поэтому он не делает предположение, что значение переменной остается неизменным после нулевой проверки и, следовательно, не может использовать smart.
Однако List.forEach
является своеобразным, потому что это inline
. Тело встроенной функции и тела ее функциональных параметров (если только параметр имеет модификаторы noinline
или crossinline
) не вставлены на сайт вызова, поэтому компилятор может рассуждать о коде в лямбда, переданном в качестве аргумента для встроенного как будто она была написана непосредственно в корпусе вызывающего метода, что делает возможным умение.
Он может, но в настоящее время это не так, просто потому, что эта функция еще не реализована. Для него есть открытая проблема: KT-7186.
Ответ 2
Спасибо Илье за подробное объяснение проблемы! Если вам нужен обходной путь, вы можете использовать стандартное выражение for(item in list){...}
следующим образом:
var max : Int? = null
val list = listOf(1, 2, 3)
for(item in list){
if (max == null || item > max) {
max = item
}
}
Ответ 3
Проблема заключается в том, что foreach
создает несколько замыканий, каждый из которых получает доступ к тому же max
, который является var
.
Что произойдет, если max
было установлено на null
в другом закрытии после проверки max == null
, но до it > max
?
Поскольку каждое замыкание теоретически может работать независимо (потенциально на нескольких потоках), но все имеют доступ к одному и тому же max
, вы не можете гарантировать, что он не изменится во время выполнения.
Ответ 4
Это похоже на ошибку компилятора.
Если встроенный лямбда-параметр в forEach
был помечен как crossinline
, тогда я ожидал бы ошибку компиляции из-за возможности одновременных вызовов lambda-выражения.
Рассмотрим следующую реализацию forEach
:
inline fun <T> Iterable<T>.forEach(crossinline action: (T) -> Unit): Unit {
val executorService: ExecutorService = ForkJoinPool.commonPool()
val futures = map { element -> executorService.submit { action(element) } }
futures.forEach { future -> future.get() }
}
Вышеупомянутая реализация не сможет скомпилироваться без модификатора crossinline
. Без него лямбда может содержать нелокальные возвращения, что означает, что он не может использоваться одновременно.
Я предлагаю создать проблему: Kotlin (KT) | YouTrack.