Ошибка компиляции: 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.