JEP 361: Switch Expressions
Выражения switch
| Автор | Gavin Bierman |
| Ответственный | Jan Lahoda |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 14 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot java dot net |
| Трудоёмкость | S |
| Длительность | M |
| Связан с | JEP 354: Switch Expressions (Second Preview) |
| JEP 325: Switch Expressions (Preview) | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2019/09/04 06:35 |
| Обновлён | 2022/03/11 20:22 |
| Задача | 8230539 |
Аннотация
Расширить switch так, чтобы его можно было использовать и как оператор, и как выражение, и чтобы обе формы могли использовать как традиционные метки case ... : (со сквозным выполнением), так и новые метки case ... -> (без сквозного выполнения), а также добавить ещё один новый оператор для выдачи значения из выражения switch. Эти изменения упростят повседневное программирование и подготовят почву для использования Pattern Matching (сопоставление с образцом) в switch. Это была возможность языка в статусе Preview (предварительная версия) в JDK 12 и JDK 13.
История
Выражения switch были предложены в декабре 2017 года в JEP 325. JEP 325 был запланирован в JDK 12 в августе 2018 года как Preview-возможность. Одним из аспектов JEP 325 была перегрузка оператора break для возврата результирующего значения из выражения switch. Отзывы о JDK 12 показали, что такое использование break сбивает с толку. В ответ на эти отзывы был создан JEP 354 как развитие JEP 325. JEP 354 предложил новый оператор yield и вернул break исходный смысл. JEP 354 был запланирован в JDK 13 в июне 2019 года как Preview-возможность. Отзывы о JDK 13 показали, что выражения switch готовы стать окончательными и постоянными в JDK 14 без дальнейших изменений.
Мотивация
Поскольку мы готовимся расширить язык программирования 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 является поли-выражением: если целевой тип известен, этот тип передаётся в каждую ветвь. Тип выражения 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.
yield — не ключевое слово, а ограниченный идентификатор (как var), поэтому классы с именем yield недопустимы. Если в области видимости есть унарный метод yield, то выражение yield(x) было бы неоднозначным (это может быть как вызов метода, так и оператор yield, операнд которого — выражение в скобках), и эта неоднозначность разрешается в пользу оператора yield. Если нужен вызов метода, метод следует квалифицировать: через this для метода экземпляра или через имя класса для статического метода.
Исчерпываемость
Варианты 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
};
...
}
Зависимости
Этот JEP вырос из JEP 325 и JEP 354. Однако этот JEP самостоятелен и не зависит от этих двух JEP.
Будущая поддержка Pattern Matching, начиная с JEP 305, будет основана на этом JEP.
Риски и допущения
Необходимость оператора switch с метками case L -> иногда неочевидна. В пользу его включения говорили следующие соображения:
-
Есть операторы
switch, которые работают за счёт побочных эффектов, но в целом всё же следуют принципу «одно действие на метку». Если перевести их на метки нового стиля, операторы станут проще и менее подвержены ошибкам. -
То, что поток управления по умолчанию в блоке оператора
switch— сквозное выполнение, а не выход из блока, было неудачным решением на раннем этапе истории Java и по-прежнему доставляет разработчикам немало беспокойства. Если решить эту проблему для конструкцииswitchв целом, а не только для выраженийswitch, последствия этого решения уменьшаются. -
Если разделить желаемые преимущества (возможность быть выражением, более удобный поток управления, более разумные области видимости) на ортогональные возможности, у выражений
switchи операторовswitchможет быть больше общего. Чем сильнее расходятся выраженияswitchи операторыswitch, тем сложнее изучать язык и тем больше острых углов, о которые разработчики могут пораниться.