Статьи

Очень своеобразная, но, возможно, хитрая особенность языка котлин

Это застало меня врасплох. После изучения языка Kotlin, чтобы узнать, как наилучшим образом использовать этот интересный новый язык для jOOQ , я наткнулся на эту головоломку. Как вы думаете, следующая программа будет печатать?

1
2
3
4
5
6
7
8
9
fun main(args: Array) {
    (1..5).forEach {
        if (it == 3)
            return
        print(it)
    }
 
    print("done")
}

Ну … Вы могли догадаться неправильно. Выше будет напечатано:

1
1 2

Он НЕ напечатает то, что может ожидать большинство людей:

1
1245done

Примечание для тех из вас, кто не удивлен:

1245done характерно для тех, кто привык работать с Java 8, где следующий код действительно 1245done :

01
02
03
04
05
06
07
08
09
10
public static void main(String[] args) {
    IntStream.rangeClosed(1, 5).forEach(it -> {
        if (it == 3)
            return;
 
        System.out.print(it);
    });
 
    System.out.print("done");
}

Синтаксическая причина объясняется в этом разделе руководства Kotlin: https://kotlinlang.org/docs/reference/returns.html

В лямбда-выражениях / замыканиях оператор return будет (не обязательно) возвращаться из лямбда / замыкания, но из непосредственной области охвата лямбда / замыкания. Обоснование было любезно предоставлено мне Дмитрием Джемеровым из JetBrains в двух твиттах:

Как ни странно, язык Kotlin удалил основанную на языке поддержку таких конструкций Java, как try-with-resources или synchronized оператор. Это очень разумно, потому что эти языковые конструкции не обязательно принадлежат языку ( как мы ранее заявляли в другом посте в блоге ), но вместо этого могут быть перемещены в библиотеки. Например:

1
2
3
4
5
// try-with-resources is emulated using an
// extension function "use"
OutputStreamWriter(r.getOutputStream()).use {
    it.write('a')
}

Или же:

1
2
// Synchronized is a function!
val x = synchronized (lock, { computation() })

В конце концов, даже в Java функция языка работает только потому, что язык зависит от типов библиотеки, таких как Iterable ( foreach ), AutoCloseable ( try-with-resources ) или функции JVM (отслеживайте каждую ссылку для synchronized ).

Итак, в чем же дело с возвращением?

В соответствии с приведенным выше обоснованием, когда разработчики языка хотят избежать языковых конструкций для вещей, которые могут быть реализованы с помощью библиотек, но все же хотят, чтобы вы чувствовали, что это языковые конструкции, тогда единственное разумное значение return внутри такой «конструкции» -ish »лямбда / замыкание — это возврат из внешней области видимости. Итак, когда вы пишете что-то вроде:

01
02
03
04
05
06
07
08
09
10
11
fun main(args : Array) {
    val lock = Object()
    val x = synchronized(lock, {
        if (1 == 1)
            return
 
        "1"
    })
 
    print(x)
}

Реальное намерение состоит в том, чтобы это было эквивалентно следующему коду Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
public static void main(String[] args) {
    Object lock = new Object();
    String x;
 
    synchronized (lock) {
        if (1 == 1)
            return;
 
        x = "1";
    }
 
    System.out.println(x);
}

В случае Java, очевидно, оператор return выходит из метода main() , потому что нет другого разумного стекового фрейма для возврата. В отличие от Kotlin, где можно утверждать, что лямбда / замыкание создаст свой собственный кадр стека.

Но это действительно не так. Причиной этого является inline модификатор synchronized функции:

1
2
3
4
5
6
7
8
9
public inline fun <R> synchronized(lock: Any, block: () -> R): R {
    monitorEnter(lock)
    try {
        return block()
    }
    finally {
        monitorExit(lock)
    }
}

Это означает, что закрытие block передаваемое в качестве аргумента, на самом деле является не просто лямбда-выражением, а просто синтаксическим сахаром, встроенным в область вызова.

Weird. Хитрость. Умная. Но немного неожиданно.

Это хорошая идея? Или дизайнеры языка будут сожалеть об этом позже? Являются ли все лямбды / замыкания потенциально «языковой конструкцией», где такой оператор возврата должен покинуть внешнюю область? Или есть явные случаи, когда это inline поведение имеет смысл?

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