openjdk.ruOpenJDK на русском

JEP 354: Switch Expressions (Second Preview)

Выражения switch (вторая версия Preview (предварительная версия))

АвторGavin Bierman
ОтветственныйJan Lahoda
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск13
Компонентspecification / language
Обсуждениеamber dash dev at openjdk dot java dot net
ТрудоёмкостьS
ДлительностьM
Связан сJEP 325: Switch Expressions (Preview)
JEP 361: Switch Expressions
РецензентыAlex Buckley, Brian Goetz
ОдобренBrian Goetz
Создан2019/04/09 12:38
Обновлён2021/08/28 00:17
Задача8222184

Аннотация

Расширить switch так, чтобы его можно было использовать и как оператор, и как выражение, причём обе формы могли использовать либо традиционные метки case ... : (с проваливанием), либо новые метки case ... -> (без проваливания). Дополнительно вводится новый оператор, который возвращает значение из выражения switch. Эти изменения упростят повседневное программирование и подготовят почву для использования Pattern Matching (сопоставление с образцом), JEP 305 в switch. Это возможность языка в статусе Preview в JDK 13.

История

Выражения switch были предложены в декабре 2017 года в JEP 325. В августе 2018 года они были запланированы в JDK 12 как Preview-возможность. Сначала мы собирали отзывы о дизайне этой возможности, а затем об опыте использования выражений switch и улучшенного оператора switch. По итогам этих отзывов этот JEP вносит в возможность одно изменение:

Для возврата значения из выражения switch вместо оператора break со значением теперь используется оператор yield.

Мотивация

Пока мы готовимся расширить язык программирования Java поддержкой Pattern Matching, JEP 305, некоторые несуразности существующего оператора switch, которые давно раздражают пользователей, становятся препятствием. Среди них: поведение потока управления между метками switch по умолчанию (проваливание), область видимости по умолчанию в блоках switch (весь блок считается одной областью видимости) и то, что switch работает только как оператор, хотя многовариантные условия часто естественнее выражать как выражения.

Текущий дизайн оператора switch в Java близко следует таким языкам, как C и C++, и по умолчанию поддерживает семантику проваливания. Хотя такой традиционный поток управления часто полезен при написании низкоуровневого кода (например, парсеров двоичных кодировок), по мере того как switch используется в более высокоуровневых контекстах, его склонность к ошибкам начинает перевешивать его гибкость. Например, в следующем коде многочисленные операторы break делают его излишне многословным, и этот визуальный шум часто скрывает трудно отлаживаемые ошибки, когда пропущенный оператор break приводит к случайному проваливанию.

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

Мы предлагаем ввести новую форму метки switch, «case L ->», которая означает, что при совпадении метки выполняется только код справа от неё. Мы также предлагаем разрешить несколько констант в одном case через запятую. Теперь предыдущий код можно записать так:

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

Код справа от метки switch «case L ->» может быть только выражением, блоком или (для удобства) оператором throw. У этого есть приятное следствие: если ветвь вводит локальную переменную, она должна находиться внутри блока и поэтому не видна ни в одной другой ветви блока switch. Так устраняется ещё одно неудобство традиционных блоков switch, где областью видимости локальной переменной является весь блок:

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // The scope of 'temp' continues to the }
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // Can't call this variable 'temp'
        break;
    default:
        int temp3 = ...    // Can't call this variable 'temp'
}

Многие существующие операторы switch по сути имитируют выражения switch: каждая ветвь либо присваивает значение общей целевой переменной, либо возвращает значение:

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}

Выражать это оператором — окольный, многословный и подверженный ошибкам путь. Автор хотел выразить, что для каждого дня нужно вычислить значение numLetters. Должна быть возможность сказать это напрямую, с помощью выражения switch, что одновременно понятнее и безопаснее:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

В свою очередь, расширение switch для поддержки выражений порождает дополнительные потребности, например расширение анализа потока управления (выражение всегда должно вычислить значение или завершиться внезапно) и возможность для некоторых ветвей case выражения switch выбрасывать исключение вместо возврата значения.

Описание

Метки-стрелки

Помимо традиционных меток «case L :» в блоке switch мы предлагаем новую упрощённую форму — метки «case L ->». Если метка совпала, выполняется только выражение или оператор справа от стрелки; проваливания нет. Например, для следующего оператора switch, использующего новую форму меток:

static void howMany(int k) {
    switch (k) {
        case 1  -> System.out.println("one");
        case 2  -> System.out.println("two");
        default -> System.out.println("many");
    }
}

Следующий код:

howMany(1);
howMany(2);
howMany(3);

выводит следующее:

one
two
many

Выражения switch

Мы расширим оператор switch, чтобы его можно было использовать как выражение. Например, предыдущий метод howMany можно переписать с использованием выражения switch, так что в нём будет только один println.

static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one"
            case  2 -> "two"
            default -> "many"
        }
    );
}

В типичном случае выражение switch выглядит так:

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

Выражение switch является poly-выражением: если целевой тип известен, он передаётся вниз в каждую ветвь. Тип выражения switch — его целевой тип, если он известен; если нет, самостоятельный тип вычисляется объединением типов всех ветвей case.

Возврат значения

У большинства выражений switch справа от метки switch «case L ->» будет одно выражение. На случай, когда нужен полноценный блок, мы вводим новый оператор yield для возврата значения, которое становится значением объемлющего выражения switch.

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

Выражение switch, как и оператор switch, может также использовать традиционный блок switch с метками «case L:» (что подразумевает семантику проваливания). В этом случае значения возвращаются с помощью нового оператора yield:

int result = switch (s) {
    case "Foo": 
        yield 1;
    case "Bar":
        yield 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        yield 0;
};

Два оператора, break (с меткой или без неё) и yield, позволяют легко различать операторы switch и выражения switch: целью оператора break может быть оператор switch, но не выражение switch; а целью оператора yield может быть выражение switch, но не оператор switch.

В предыдущей версии Preview выражений switch, JEP 325, мы предлагали добавить новую форму оператора break со значением, которая использовалась бы для возврата значения из выражения switch. В этой версии выражений switch она будет заменена новым оператором yield.

Исчерпываемость

Набор case выражения switch должен быть исчерпывающим: для любого возможного значения должна быть подходящая метка switch. (Разумеется, от операторов switch исчерпываемость не требуется.)

На практике это обычно означает, что требуется ветвь default; однако в случае выражения switch по enum, которое охватывает все известные константы, компилятор вставляет ветвь default, указывающую, что определение enum изменилось между компиляцией и выполнением. Опора на такую неявную вставку ветви default делает код надёжнее: теперь при перекомпиляции кода компилятор проверяет, что все случаи обработаны явно. Если бы разработчик вставил явную ветвь default (как это делается сегодня), возможная ошибка была бы скрыта.

Кроме того, выражение switch должно либо завершиться нормально со значением, либо завершиться внезапно, выбросив исключение. Из этого следует ряд последствий. Во-первых, компилятор проверяет, что для каждой метки switch при её совпадении может быть возвращено значение.

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // ERROR! Block doesn't contain a yield statement
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // ERROR! Group doesn't contain a yield statement
};

Ещё одно следствие: управляющие операторы break, yield, return и continue не могут выполнять переход сквозь выражение switch, как в следующем примере:

z: 
    for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                yield 1;
            case 1:
                yield 2;
            default: 
                continue z; 
                // ERROR! Illegal jump through a switch expression 
        };
    ...
    }

Зависимости

Эта возможность была в статусе Preview в JEP 325.

От этого JEP зависит Pattern Matching (JEP 305).

Риски и допущения

Необходимость оператора switch с метками case L -> иногда неочевидна. Ниже изложены допущения, на которых основано его включение:

  • Существуют операторы switch, которые работают за счёт побочных эффектов, но в целом всё же следуют принципу «одно действие на метку». Если перевести их на метки нового стиля, операторы станут проще и менее подвержены ошибкам.

  • То, что по умолчанию поток управления в блоке оператора switch проваливается дальше, а не выходит из блока, было неудачным решением, принятым на раннем этапе истории Java. Это источник огромных мучений для разработчиков. Казалось, что эту проблему следует решить для конструкции switch в целом, а не только для выражений switch.

  • Представлялось более предпочтительным разделить желаемые преимущества (возможность быть выражением, лучший поток управления, более разумные области видимости) на ортогональные возможности, чтобы у выражений switch и операторов switch было больше общего. Чем сильнее расходятся выражения switch и операторы switch, тем сложнее изучать язык и тем больше острых углов, о которые разработчики могут пораниться.