Оператор switch
Вторым оператором выбора в C# является оператор switch
, который обеспечивает многонаправленное ветвление программы. Следовательно, этот оператор позволяет сделать выбор среди нескольких альтернативных вариантов дальнейшего выполнения программы. Несмотря на то, что многонаправленная проверка может быть организована с помощью последовательного ряда вложенных операторов if
, во многих случаях более эффективным оказывается применение оператора switch
. Этот оператор действует следующим образом. Значение выражения последовательно сравнивается с константами выбора из заданного списка. Как только будет обнаружено совпадение с одним из условий выбора, выполняется последовательность операторов, связанных с этим условием. Ниже приведена общая форма оператора switch
.
switch(выражение) {
case константа1:
последовательность операторов
break;
case константа2:
последовательность операторов
break;
case константа3:
последовательность операторов
break;
.
.
.
default:
последовательность операторов
break;
}
Заданное выражение
в операторе switch
должно быть целочисленного типа (char
, byte
, short
или int
), перечислимого или же строкового. (О перечислениях и символьных строках типа string
речь пойдет далее в этом курсе.) А выражения других типов, например с плавающей точкой, в операторе switch
не допускаются.
Зачастую выражение, управляющее оператором switch
, просто сводится к одной переменной. Кроме того, константы выбора должны иметь тип, совместимый с типом выражения. В одном операторе switch
не допускается наличие двух одинаковых по значению констант выбора.
Последовательность операторов из ветви default
выполняется в том случае, если ни одна из констант выбора не совпадает с заданным выражением. Ветвь default
не является обязательной. Если же она отсутствует и выражение не совпадает ни с одним из условий выбора, то никаких действий вообще не выполняется. Если же происходит совпадение с одним из условий выбора, то выполняются операторы, связанные с этим условием, вплоть до оператора break
.
Ниже приведен пример программы, в котором демонстрируется применение оператора switch
.
// Продемонстрировать применение оператора switch,
using System;
class SwitchDemo {
static void Main() {
int i;
for (i = 0; i < 10; i++)
switch (i) {
case 0:
Console.WriteLine("i равно нулю");
break;
case 1:
Console.WriteLine("i равно единице");
break;
case 2:
Console.WriteLine("i равно двум");
break;
case 3:
Console.WriteLine("i равно трем");
break;
case 4:
Console.WriteLine("i равно четырем");
break;
default:
Console.WriteLine("i равно или больше пяти");
break;
}
}
}
Результат выполнения этой программы выглядит следующим образом.
i равно нулю
i равно единице
i равно двум
i равно трем
i равно четырем
i равно или больше пяти
i равно или больше пяти
i равно или больше пяти
i равно или больше пяти
i равно или больше пяти
Как видите, на каждом шаге цикла выполняются операторы, связанные с совпадающей константой выбора, в обход всех остальных операторов. Когда же значение переменной i
становится равным или больше пяти, то оно не совпадает ни с одной из констант выбора, а следовательно, выполняются операторы из ветви default
.
В приведенном выше примере оператором switch
управляла переменная i
типа int
. Как пояснялось ранее, для управления оператором switch
может быть использовано выражение любого целочисленного типа, включая и char
. Ниже приведен пример применения выражения и констант выбора типа char
в операторе switch
.
// Использовать элементы типа char для управления оператором switch,
using System;
class SwitchDemo2 {
static void Main() {
char ch;
for (ch = 'A'; ch <= 'E'; ch++)
switch (ch) {
case 'A':
Console.WriteLine("ch содержит A");
break;
case 'B':
Console.WriteLine("ch содержит B");
break;
case 'C':
Console.WriteLine("ch содержит C");
break;
case 'D':
Console.WriteLine("ch содержит D");
break;
case 'E':
Console.WriteLine("ch содержит E");
break;
}
}
}
Вот какой результат дает выполнение этой программы.
ch содержит A
ch содержит B
ch содержит C
ch содержит D
ch содержит E
Обратите в данном примере внимание на отсутствие ветви default
в операторе switch
. Напомним, что ветвь default
не является обязательной. Когда она не нужна, ее можно просто опустить.
Переход последовательности операторов, связанных с одной ветвью case
, в следующую ветвь case
считается ошибкой, поскольку в C# должно непременно соблюдаться правило недопущения «провалов» в передаче управления ходом выполнения программы. Именно поэтому последовательность операторов в каждой ветви case
оператора switch
оканчивается оператором break
. (Избежать подобных «провалов», можно также с помощью оператора безусловного перехода goto
, рассматриваемого далее в этой главе, но для данной цели чаще применяется оператор break
.)
Когда в последовательности операторов отдельной ветви case
встречается оператор break
, происходит выход не только из этой ветви, но из всего оператора switch
, а выполнение программы возобновляется со следующего оператора, находящегося за пределами оператора switch
. Последовательность операторов в ветви default
также должна быть лишена “провалов”, поэтому она завершается, как правило, оператором break
.
Правило недопущения «провалов» относится к тем особенностям языка C#, которыми он отличается от C, C++ и Java. В этих языках программирования одна ветвь case
может переходить (т. е. «проваливаться») в другую. Данное правило установлено в C# для ветвей case
по двум причинам.
Во-первых, оно дает компилятору возможность свободно изменять порядок следования последовательностей операторов из ветвей case
для целей оптимизации. Такая реорганизация была бы невозможной, если бы одна ветвь case
могла переходить в другую.
И во-вторых, требование завершать каждую ветвь case
явным образом исключает непроизвольные ошибки программирования, допускающие переход одной ветви case в другую.
Несмотря на то, что правило недопущения «провалов» не допускает переход одной ветви case
в другую, в двух или более ветвях case
все же разрешается ссылаться с помощью меток на одну и ту же кодовую последовательность, как показано в следующем примере программы.
// Пример "проваливания" пустых ветвей case.
using System;
class EmptyCasesCanFall {
static void Main() {
int i;
for (i = 1; i < 5; i++)
switch (i) {
case 1:
case 2:
case 3:
Console.WriteLine("i равно 1, 2 или 3");
break;
case 4:
Console.WriteLine("i равно 4");
break;
}
}
}
Ниже приведен результат выполнения этой программы.
i равно 1, 2 или 3
i равно 1, 2 или 3
i равно 1, 2 или 3
i равно 4
Если значение переменной i
в данном примере равно 1, 2 или 3, то выполняется первый оператор, содержащий вызов метода WriteLine()
. Такое расположение нескольких меток ветвей case
подряд не нарушает правило недопущения «провалов»; поскольку во всех этих ветвях используется одна и та же последовательность операторов.
Расположение нескольких меток ветвей case
подряд зачастую применяется в том случае, если у нескольких ветвей имеется общий код. Благодаря этому исключается излишнее дублирование кодовых последовательностей.
Switch case statement evaluates a given expression and based on the evaluated value(matching a certain condition), it executes the statements associated with it. Basically, it is used to perform different actions based on different conditions(cases).
- Switch case statements follow a selection-control mechanism and allow a value to change control of execution.
- They are a substitute for long if statements that compare a variable to several integral values.
- The switch statement is a multiway branch statement. It provides an easy way to dispatch execution to different parts of code based on the value of the expression.
In C, the switch case statement is used for executing one condition from multiple conditions. It is similar to an if-else-if ladder.
The switch statement consists of conditional-based cases and a default case.
Syntax of switch Statement in C
switch(expression) { case value1: statement_1; break; case value2: statement_2; break; . . . case value_n: statement_n; break; default: default_statement; }
How to use switch case Statement in C?
Before using the switch case in our program, we need to know about some rules of the switch statement.
Rules of the switch case statement
Following are some of the rules that we need to follow while using the switch statement:
- In a switch statement, the “case value” must be of “char” and “int” type.
- There can be one or N number of cases.
- The values in the case must be unique.
- Each statement of the case can have a break statement. It is optional.
- The default Statement is also optional.
Example
C
#include <stdio.h>
int
main()
{
int
var = 1;
switch
(var) {
case
1:
printf
(
"Case 1 is Matched."
);
break
;
case
2:
printf
(
"Case 2 is Matched."
);
break
;
case
3:
printf
(
"Case 3 is Matched."
);
break
;
default
:
printf
(
"Default case is Matched."
);
break
;
}
return
0;
}
How switch Statement Work?
The working of the switch statement in C is as follows:
- Step 1: The switch variable is evaluated.
- Step 2: The evaluated value is matched against all the present cases.
- Step 3A: If the matching case value is found, the associated code is executed.
- Step 3B: If the matching code is not found, then the default case is executed if present.
- Step 4A: If the break keyword is present in the case, then program control breaks out of the switch statement.
- Step 4B: If the break keyword is not present, then all the cases after the matching case are executed.
- Step 5: Statements after the switch statement are executed.
We can also understand the working of the switch statement in C using the flowchart.
Flowchart of Switch Statement
Flowchart of switch statement in C
Break in switch case
This keyword is used to stop the execution inside a switch block. It helps to terminate the switch block and break out of it. When a break statement is reached, the switch terminates, and the flow of control jumps to the next line following the switch statement.
The break statement is optional. If omitted, execution will continue on into the next case. The flow of control will fall through to subsequent cases until a break is reached.
Example of switch case without break
C
#include <stdio.h>
int
main()
{
int
var = 2;
switch
(var) {
case
1:
printf
(
"Case 1 is executed.\n"
);
case
2:
printf
(
"Case 2 is executed.\n"
);
case
3:
printf
(
"Case 3 is executed."
);
case
4:
printf
(
"Case 4 is executed."
);
}
return
0;
}
Output
Case 2 is executed. Case 3 is executed.Case 4 is executed.
Default in switch case
The default keyword is used to specify the set of statements to execute if there is no case match.
It is optional to use the default keyword in a switch case. Even if the switch case statement does not have a default statement, it would run without any problem.
Important Points About Switch Case Statements
1. Switch expression should result in a constant value
If the expression provided in the switch statement does not result in a constant value, it would not be valid. Some valid expressions for switch case will be,
// Constant expressions allowed switch(1+2+23) switch(1*2+3%4) // Variable expression are allowed provided // they are assigned with fixed values switch(a*b+c*d) switch(a+b+c)
2. Expression value should be only of int or char type.
The switch statement can only evaluate the integer or character value. So the switch expression should return the values of type int or char only.
3. Case Values must be Unique
In the C switch statement, duplicate case values are not allowed.
3. Nesting of switch Statements
Nesting of switch statements is allowed, which means you can have switch statements inside another switch. However nested switch statements should be avoided as it makes the program more complex and less readable.
Examples of switch Statement in C
Example 1: C Program to print the day of the week using a switch case.
C
#include <stdio.h>
int
main()
{
int
day = 2;
printf
(
"The day with number %d is "
, day);
switch
(day) {
case
1:
printf
(
"Monday"
);
break
;
case
2:
printf
(
"Tuesday"
);
break
;
case
3:
printf
(
"Wednesday"
);
break
;
case
4:
printf
(
"Thursday"
);
break
;
case
5:
printf
(
"Thursday"
);
break
;
case
6:
printf
(
"Thursday"
);
break
;
case
7:
printf
(
"Thursday"
);
break
;
default
:
printf
(
"Invalid Input"
);
break
;
}
return
0;
}
Output
The day with number 2 is Tuesday
Example 2: Simple Calculator using switch case in C
C
#include <stdio.h>
#include <stdlib.h>
int
main()
{
char
choice;
int
x, y;
while
(1) {
printf
(
"Enter the Operator (+,-,*,/)\nEnter x to "
"exit\n"
);
scanf
(
" %c"
, &choice);
if
(choice ==
'x'
) {
exit
(0);
}
printf
(
"Enter the two numbers: "
);
scanf
(
"%d %d"
, &x, &y);
switch
(choice) {
case
'+'
:
printf
(
"%d + %d = %d\n"
, x, y, x + y);
break
;
case
'-'
:
printf
(
"%d - %d = %d\n"
, x, y, x - y);
break
;
case
'*'
:
printf
(
"%d * %d = %d\n"
, x, y, x * y);
break
;
case
'/'
:
printf
(
"%d / %d = %d\n"
, x, y, x / y);
break
;
default
:
printf
(
"Invalid Operator Input\n"
);
}
}
return
0;
}
Output
Enter the operator (+, -, *, /) Enter x to exit + Enter the two numbers: 100 + 200 100 + 200 = 300
Advantages of C switch Statement
- Easier to read than if else if.
- Easier to debug and maintain for a large number of conditions.
- Faster execution speed.
Disadvantages of C switch Statement
- Switch case can only evaluate int or char type.
- No support for logical expressions.
- Have to keep in mind to add a break in every case.
Conclusion
In this article, we discussed the switch statement in C programming and how to use it. It is a conditional statement like the if-else-if ladder having its own merits and demerits. It is mostly preferred when the number of conditions to evaluate is large.
FAQs on C switch Statement
1. What is the switch case in C?
The switch case statement is a flow control statement in which we can define a switch variable and then execute different code based on the value of the switch variable. It is an alternative of if else if ladder.
2. What is the case in the switch statement in C?
The case keyword is used to define the different cases and their associated code in the switch statement.
3. What does the break in the switch case do?
The break keyword is used to exit the switch block after executing the matching case.
4. What are the differences between switch and if else if ladder in C?
Following are the main differences between C switch and C if else if ladder:
switch |
if else if |
---|---|
It executes the different cases on the basis of the value of the switch variable. | It executes the different blocks based on the condition specified. |
It can only evaluate the int or char type expressions. | It can evaluate any type of expression. |
Faster and easier to read for the large number of conditions. | It can get messy when there are lots of conditions. |
Must Read:
- Interesting Facts About Switch Case in C
- What should be Data type of Case Labels of Switch Statement in C?
- Print Individual Digits as Words Without Using if or Switch
Last Updated :
30 Mar, 2023
Like Article
Save Article
Содержание
- 1. Какое назначение в программах имеет оператор выбора switch?
- 2. Какая общая форма оператора выбора switch?
- 3. Принцип работы оператора switch
- 4. Пример использования оператора выбора switch, в котором есть блок default
- 5. Пример использования оператора switch, в котором пропущены операторы break
- 6. Пример использования оператора switch, в котором отсутствует блок default
- 7. Вложенные инструкции switch
- 8. Преимущества оператора switch в сравнении с оператором if
- 9. Недостатки оператора switch в сравнении с оператором if
- Связанные темы
Поиск на других ресурсах:
1. Какое назначение в программах имеет оператор выбора switch?
Оператор выбора switch есть близок к оператору условного перехода if — else. Он позволяет организовать разветвление процесса выполнения в программе. В некоторых случаях использование оператора выбора switch дает более компактный программный код в сравнении с оператором условного перехода if — else.
⇑
2. Какая общая форма оператора выбора switch?
В C-ориентированных языках программирования, к которым принадлежит и Java, оператор выбора switch имеет одинаковую общую форму представления:
switch (выражение) { case значение1: // операторы ... break; case значение2: // операторы ... break; ... case значениеN: // операторы ... break; default: // операторы, которые выполняются по умолчанию ... }
где
- выражение – управляющее выражение, которое может иметь один из целочисленных типов: byte, int, short, char или перечислительный тип. В новых версиях JDK выражение может иметь тип String.
- значение1, значение2, …, значениеN – константные выражения (литеральные значения). Каждое значение должно быть совместимым по типу с указанным выражением.
Блок default может отсутствовать.
⇑
3. Принцип работы оператора switch
Оператор выбора switch работает по следующему принципу. Значение выражения сравнивается с любым из значений (значение1, значение2, …, значениеN), которые следуют после ключевого слова case. Если найдено совпадение, тогда выполняются операторы, которые следуют после этой части case. Если ни в одном из вариантов после case совпадение не найдено, тогда выполняются операторы, которые идут в блоке default. Если блок default отсутствует (блок может отсутствовать), тогда ничего не происходит и выполняется следующий оператор, который следует за оператором switch.
Оператор break необходим для немедленного выхода из оператора switch. Если выполняется оператор break, тогда выполняется следующий после switch оператор.
⇑
4. Пример использования оператора выбора switch, в котором есть блок default
По заданному значению n = 1..7 вывести название соответствующего дня недели. Учесть возможные ошибочные значения n.
// оператор выбора switch int n; String s; // ввод значения n n = 6; switch (n) { case 1: s = "Понедельник"; break; case 2: s = "Вторник"; break; case 3: s = "Среда"; break; case 4: s = "Четверг"; break; case 5: s = "Пятница"; break; case 6: s = "Суббота"; break; case 7: s = "Воскресенье"; break; default: s = "Неправильно введен день"; } System.out.println(s);
⇑
5. Пример использования оператора switch, в котором пропущены операторы break
Как известно, оператор break, может отсутствовать.
В данном примере по введенному значению номера дня недели n = 1..7 определяется выходной этот день или рабочий.
// оператор выбора switch int n; String s; // ввод значения n n = 3; switch (n) { case 1: case 2: case 3: case 4: case 5: s = "Рабочий день"; break; case 6: case 7: s = "Выходной день"; break; default: s = "Неправильно введен день"; } System.out.println(s);
⇑
6. Пример использования оператора switch, в котором отсутствует блок default
Дано целое число n = 1..3. По данному значению переменной n определить:
- длину окружности;
- площадь круга;
- объем шара.
// оператор выбора switch int n; double pi = 3.1415; double r; // ввод значений n, r // ... switch (n) { case 1: double d = 2*pi*r; System.out.println("Длина окружности = " + d); break; case 2: double s = pi*r*r; System.out.println("Площадь круга = " + s); break; case 3: double v = 4.0/3.0 * pi*r*r*r; System.out.println("Объем шара = " + v); break; }
⇑
7. Вложенные инструкции switch. Пример
Инструкция switch может содержать другую, вложенную инструкцию switch. В каждом операторе switch определяется свой блок кода. Блоки кода есть независимыми друг от друга.
Пример. В данном примере на основании номера года (year) и месяца (month) вычисляется количество дней в месяце (переменная days). Задача решена с использованием вложенных операторов if и switch
// вычисление количества дней в месяце года int f; int year; int month; int days; // заданы месяц и год year = 2404; month = 2; switch(month) { case 4: case 6: case 9: case 11: days = 30; break; case 2: // месяц февраль f = 0; // вложенный оператор if if (year%400==0) f=1; else if (year%100==0) f=0; else if (year%4==0) f=1; // вложенный оператор switch switch (f) { case 0: days=28; break; default: days=29; } break; default: days = 31; } System.out.println("Days = " + days);
⇑
8. Преимущества оператора switch в сравнении с оператором if
В сравнении с оператором if преимущества оператора switch следующие:
- при выборе из большой группы значений оператор switch работает более быстрее чем оператор if-else. Это связано с тем, что в операторе switch константы всех ветвей case и выражение имеют одинаковый тип. Поэтому достаточно осуществить проверку на равенство. В операторе if-else компилятор заведомо не знает типы результатов выражений в разных ветвях сравнения, что требует дополнительных преобразований занимающих время.
- в случаях когда выполняется код для нескольких операторов ветвей case без указания разделяющих их операторов break.
⇑
9. Недостатки оператора switch в сравнении с оператором if
В сравнении с оператором if, оператор switch имеет следующие недостатки:
- в операторе switch, в каждой ветви выполняется проверка только на равенство (поиск на совпадение в ветви case). В операторе if в каждой ветви можно вычислять любое логическое выражение любого типа;
- в операторе switch выражение, которое сравнивается может быть только типа int, перечислением enum или типа String (начиная с версии JDK 7). Выражение не может быть типом с плавающей запятой. В операторе if сравниваемое выражение может быть любого типа;
- в двух разных ветвях case константы не могут иметь одинаковые значения. Одинаковые значения в ветвях case допускаются, только когда эти ветви размещаются на разных уровнях вложения в случае вложенных операторов switch.
⇑
Связанные темы
- Оператор условного перехода if
- Конструкция if-else-if
- Операции. Операция присваивания. Тернарная операция ? : . Приоритет операций
- C++. Оператор выбора switch
⇑
Оператор выбора switch
(часто его называют переключателем)
предназначен для выбора ветви
вычислительного процесса исходя из
значения управляющего выражения. При
этом значение управляющего выражения
сравнивается со значениями в списке
целых или символьных констант. Если
будет найдено совпадение, то выполнится
ассоциированный с совпавшей константой
оператор.
Общая форма оператора switchследующая:
switch
(выражение)
{
case
постоянная1:
последовательность
операторов
break;
case
постоянная2:
последовательность
операторов
break;
case
постоянная3:
последовательность
операторов
break;
default:
последовательность
операторов;
}
Значение выражения оператора switch должно
быть таким, чтобы его можно было выразить
целым числом. Это означает, что в
управляющем выражении можно использовать
переменные целого или символьного типа,
но только не с плавающей точкой.
Значение управляющего выражения по
очереди сравнивается с постоянными в
операторах case.
Если значение управляющего выражения
совпадет с какой-то из постоянных,
управление передается на соответствующую
метку case и выполняется последовательность
операторов до оператора break. Если оператор
break отсутствует, выполнение последовательности
операторов продолжается до тех пор,
пока не встретится break (в другой метке)
или не кончится тело оператора switch (т.е.
блок, следующий за switch).
Оператор default выполняется в том случае,
когда значение управляющего выражения
не совпало ни с одной постоянной. Оператор
default также может отсутствовать. В этом
случае при отсутствии совпадений не
выполняется ни один оператор.
Оператор case
— это метка, однако он не может быть
использован сам по себе, вне оператораswitch.
Оператор break
— это один из операторов безусловного
перехода. Он может применяться не только
в оператореswitch,но и в циклах, (см. раздел «Операторы
цикла»). Когда в теле оператораswitchвстречается операторbreak,
программа выходит из оператораswitchи выполняет оператор, следующий за
фигурной скобкой}оператораswitch.
Об операторе switchочень важно помнить следующее:
-
Оператор switchотличается отifтем, что в нем управляющее выражение
проверяется только на равенство с
постоянными, в то время как вifпроверяется любой вид отношения или
логического выражения. -
В одном и том же операторе switchникакие два оператораcase
не могут иметь равных
постоянных. Конечно, если одинswitchвложен в другой, в их операторахcaseмогут быть совпадающие постоянные. -
Если в управляющем выражении оператора
switch
встречаются символьные константы, они
автоматически преобразуются к целому
типу по принятым в языке С правилам
приведения типов.
Оператор switchчасто используется для обработки команд
с клавиатуры, например, при выборе
пунктов меню.
В примере программа выводит на экран
меню проверки правописания и вызывает
соответствующую процедуру:
void
menu(void)
{
char
ch;
printf(«1.
Проверка правописания\n»);
printf(«2.
Коррекция ошибок\n»);
printf(«3.
Вывод ошибок\n»);
printf(«Для
пропуска нажмите любую клавишу\n»);
printf(»
Введите Ваш выбор: «);
ch
= getchar(); /* чтение клавиш */
switch(ch)
{
case
‘1’:
check_spelling();
break;
case
‘2’:
correct_errors();
break;
case
‘3’:
display_errors();
break;
default
:
printf(«Ни
выбрана ни одна опция»);
}
}
С точки зрения синтаксиса, присутствие
операторов break
внутри switch
не обязательно. Они
прерывают выполнение последовательности
операторов, ассоциированных с данной
константой.
Если оператор break
отсутствует, то выполняется
следующий оператор case,
пока не встретится очередной break,
или не будет достигнут конец тела
оператора switch.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
Время на прочтение
11 мин
Количество просмотров 58K
Старый добрый switch был в Java с первого дня. Мы все используем его и привыкли к нему — особенно к его причудам (кого-нибудь еще раздражает break?). Но начиная с Java 12, ситуация начала меняться: switch вместо оператора стал выражением:
boolean result = switch(ternaryBool) {
case TRUE -> true;
case FALSE -> false;
case FILE_NOT_FOUND -> throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
default -> throw new IllegalArgumentException("Seriously?!");
};
Результат работы switch-выражения теперь можно сохранять в переменную; ушла необходимость использовать break в каждой ветке case благодаря лямбда-синтаксису и многое другое.
Когда дело доходит до switch после Java 14, необходимо выбрать стиль его использования:
- оператор или выражение (с Java 14)
- двоеточия или стрелки (с Java 14)
- метки или шаблоны (3-й превью в Java 19)
В этом руководстве я расскажу обо всем, что необходимо знать о switch-выражениях, и как их лучше всего использовать в современной Java.
Недостатки оператора switch
Прежде, чем мы перейдем к обзору нововведений, давайте рассмотрим один пример кода. Допустим, мы столкнулись с «ужасным» тернарным boolean и хотим преобразовать его в обычный boolean. Вот один из способов сделать это:
boolean result;
switch(ternaryBool) {
case TRUE:
result = true;
break;
case FALSE:
result = false;
break;
case FILE_NOT_FOUND:
// объявление переменной для демонстрации проблемы в default
var ex = new UncheckedIOException("This is ridiculous!",
new FileNotFoundException());
throw ex;
default:
// А вот и проблема: мы не можем объявить еще одну переменную с именем ex
var ex2 = new IllegalArgumentException("Seriously?!");
throw ex2;
}
Реализация данного кода хромает: наличие break в каждой ветке, которые легко забыть; можно не учесть все возможные значения ternaryBool (забыть реализовать какой-то case); с переменной result не все гладко — область видимости не соответствует ее использованию; нельзя объявить в разных ветках переменные с одинаковым именем. Согласитесь, что данное решение выглядит крайне громоздко и неудобно — тут явно есть, что улучшить.
А вот пример попроще, демонстрирующий похожие проблемы:
int result;
switch (number) {
case 1:
result = callMethod("one");
break;
case 2:
result = callMethod("two");
break;
default:
result = callMethod("many");
break;
}
Давайте попробуем устранить все недостатки, поместив switch в отдельный метод:
private static boolean toBoolean(Bool ternaryBool) {
switch(ternaryBool) {
case TRUE: return true;
case FALSE: return false;
case FILE_NOT_FOUND:
throw new UncheckedIOException("This is ridiculous!",
new FileNotFoundException());
// без default метод не скомпилируется
default:
throw new IllegalArgumentException("Seriously?!");
}
}
Так намного лучше: отсутствует фиктивная переменная result, нет break, загромождающих код и сообщений компилятора об отсутствии default (даже если в этом нет необходимости, как в данном случае).
Но если подумать, то мы не обязаны создавать методы только для того, чтобы обойти неуклюжую особенность языка. И это даже без учёта, что такой рефакторинг не всегда возможен. Нет, нам нужно решение получше!
Представляем switch-выражения!
Начиная с Java 12 и выше, вы можете решить вышеуказанные проблемы следующим образом:
boolean result = switch(ternaryBool) {
case TRUE -> true;
case FALSE -> false;
case FILE_NOT_FOUND -> throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
// в ветке `default` уже нет необходимости
default -> throw new IllegalArgumentException("Seriously?!");
};
Я думаю, что это довольно очевидно: если ternartBool равен TRUE, то result будет присвоено true, а FALSE становится false.
Сразу возникают две мысли:
- switch теперь может иметь результат
- какие возможности предоставляют стрелки?
Прежде чем углубляться в детали новых возможностей, вначале я расскажу об этих двух аспектах.
Выражение vs оператора
Возможно, вы удивлены, что switch теперь является выражением. А чем же он был до этого? До Java 12 switch был оператором — императивной конструкцией, управляющей исполняющим потоком.
Думайте о различиях старой и новой версии switch, как о разнице между if и тернарным оператором. Они оба проверяют логическое условие и выполняют ту или иную ветку в зависимости от его результата.
Разница состоит в том, что if просто выполняет соответствующий блок, тогда как тернарный оператор возвращает какой-то результат:
if(condition) {
result = doThis();
} else {
result = doThat();
}
result = condition ? doThis() : doThat();
То же самое и у switch: до Java 12, если вы хотели вычислить значение и сохранить результат, то должны были либо присвоить его переменной, либо вернуть из метода, созданного специально для оператора switch.
Теперь же результат вычислений оператора switch может быть присвоен переменной.
Еще одно отличие заключается в том, что поскольку выражение является частью оператора, то оно должно заканчиваться точкой с запятой, в отличие от классического оператора switch.
Стрелка vs двоеточия
В самом начале статьи использовался пример с новым синтаксисом в лямбда-стиле со стрелкой между меткой и выполняющейся частью. Эквивалентный ему код без лямбда-стиля можно записать так:
boolean result = switch (ternaryBool) {
case TRUE:
yield true;
case FALSE:
yield false;
case FILE_NOT_FOUND:
throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
default:
throw new IllegalArgumentException("Seriously?!");
};
Обратите внимание, что вам нужно использовать новое ключевое слово yield, чтобы вернуть значение из ветки case (этот синтаксис появился в Java 13. В Java 12 вместо yield применялся break, т. е. break true вместо yield true, что выглядело странно).
Исторически сложилось, что метки с двоеточием определяют точку входа в блок операторов. С этого места начинается выполнение всего кода ниже, даже когда встречается другая метка (при отсутствии break). Механизм такой работы известен, как сквозной переход к следующему case. Для его прерывания нужен break или return.
Использование же стрелки позволяет выполнять только блок справа от нее. И никакого «проваливания».
Подробнее об эволюции switch
Несколько меток на case
Отсутствие break в case часто используется для применения одинакового поведение к веткам с разными метками. При этом программа будет переходить к следующему case, пока не наткнется на break. Из этого можно сделать вывод, что оператор switch в каждом case поддерживает наличие только одной метки:
switch (number) {
case 1:
case 2:
callMethod("few");
break;
default:
callMethod("many");
break;
}
А в новом switch один case может соответствовать нескольким меткам:
String result = switch(ternaryBool) {
case TRUE, FALSE -> "sane";
default -> "insane";
};
Поведение этого кода очевидно: TRUE и FALSE приводят к одному и тому же результату — вычисляется выражение «sane».
Подробнее о стрелке
Начиная с Java 14, switch позволяет использовать лямбда-стрелку для «сопоставления» case с кодом:
switch (number) {
case 1 -> callMethod("one");
case 2 -> callMethod("two");
default -> callMethod("many");
}
Давайте рассмотрим два свойства, характерных для стрелочной формы записи разделителя:
- отсутствие сквозного перехода к следующему case
- блоки операторов
Отсутствие сквозного перехода к следующему case
Вот, что говорится в JEP 325 об этом:
…Текущий дизайн оператора switch в Java тесно связан с такими языками, как C и C++ и по умолчанию поддерживает сквозную семантику. Хотя этот традиционный способ управления часто полезен для написания низкоуровневого кода (такого как парсеры для двоичного кодирования), поскольку switch используется в коде более высокого уровня, ошибки такого подхода начинают перевешивать его гибкость.
Я полностью согласен и приветствую возможность использовать switch без поведения по умолчанию:
switch(ternaryBool) {
case TRUE, FALSE -> System.out.println("Bool was sane");
default -> System.out.println("Bool was insane");
};
Стрелка позволяет вывести «Bool was sane» в единственном экземпляре, в то время, как с двоеточием это же сообщение отобразилось бы дважды.
Блоки операторов
Как и в случае с лямбдами, стрелка может указывать либо на один оператор (как выше), либо на блок, выделенный фигурными скобками:
boolean result = switch (Bool.random()) {
case TRUE -> {
System.out.println("Bool true");
yield true;
}
case FALSE -> {
System.out.println("Bool false");
yield false;
}
case FILE_NOT_FOUND -> {
var ex = new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
throw ex;
}
default -> {
var ex = new IllegalArgumentException(
"Seriously?!");
throw ex;
}
};
Блоки необходимы для использования более одной строки кода в case. При этом они имеют дополнительное преимущество — позволяют создавать одинаковые имена переменных в разных ветках за счет локальной области видимости для каждой ветки.
Если вам показался необычным способ выхода из блоков с помощью yield, а не return, то это необходимо, чтобы избежать путаницы: return может быть неправильно истолкован, как выход из метода. Мы лишь завершаем работу switch, оставаясь в том же методе.
Подробнее о выражениях switch
И последнее, но не менее важное — особенности использования switch в качестве выражения:
- множественные выражения
- ранний возврат
- охват всех значений (исчерпываемость)
Множественные выражения
Switch-выражения являются множественными выражениями. Это означает, что они не имеют своего собственного типа, но могут быть одним из нескольких типов. Наиболее часто в качестве таких выражений используются лямбда-выражения: s -> s + » «, могут быть и Function<String, String>, и Function<Serializable, и Object> или UnaryOperator.
Тип switch-выражения определяется исходя из типов его веток, а также из места его использования. Если результат работы switch-выражения присваивается типизированной переменной, передается в качестве аргумента или используется в контексте, где известен точный тип (целевой тип), то все его ветки должны соответствовать этому типу. Вот, что мы делали до сих пор:
String result = switch (ternaryBool) {
case TRUE, FALSE -> "sane";
default -> "insane";
};
Как итог — switch присваивается переменной String result. Следовательно, String является целевым типом, и все ветки должны возвращать результат этого типа.
То же самое происходит и здесь:
Serializable serializableMessage = switch (bool) {
case TRUE, FALSE -> "sane";
// note that we don't throw the exception!
// but it's `Serializable`, so it matches the target type
default -> new IllegalArgumentException("insane");
};
А что произойдет сейчас?
// compiler infers super type of `String` and
// `IllegalArgumentException` ~> `Serializable`
var serializableMessage = switch (bool) {
case TRUE, FALSE -> "sane";
// note that we don't throw the exception!
default -> new IllegalArgumentException("insane");
};
Про применение типа var можно прочитать в статье: «26 рекомендаций по использованию типа var в Java».
Если целевой тип неизвестен из-за использования var, то он вычисляется путем нахождения наиболее конкретного супертипа из типов, создаваемых ветками.
Ранний возврат
Следствием различия между выражением и оператором switch является то, что вы можете использовать return для выхода из оператора switch:
public String sanity(Bool ternaryBool) {
switch (ternaryBool) {
// `return` is only possible from block
case TRUE, FALSE -> { return "sane"; }
default -> { return "This is ridiculous!"; }
};
}
А вот внутри выражения использовать return уже не получится:
public String sanity(Bool ternaryBool) {
String result = switch (ternaryBool) {
// this does not compile - error:
// "return outside of enclosing switch expression"
case TRUE, FALSE -> { return "sane"; }
default -> { return "This is ridiculous!"; }
};
}
Это имеет смысл независимо от того, используете ли вы стрелку или двоеточие.
Охват всех значений (исчерпываемость)
Если вы используете switch в качестве оператора, тогда не имеет значения, охвачены все варианты или нет. Конечно, вы можете случайно пропустить case, и код будет работать неправильно, но компилятору все равно — вы, ваша IDE и ваши инструменты анализа кода останетесь с этим наедине.
Switch-выражения усугубляют эту проблему. Куда следует перейти switch, если нужная метка отсутствует? Единственный ответ, который может дать Java — это возвращать null для ссылочных типов и значение по умолчанию для примитивов. Это породило бы множество ошибок в основном коде.
Чтобы предотвратить такой исход, компилятор может помочь вам. Для switch-выражений компилятор будет настаивать, чтобы все возможные варианты были охвачены. Для каждого возможного значения переменной switch должна быть ветвь — это называется исчерпываемостью. Давайте посмотрим на пример, который может привести к ошибке компиляции:
// compile error:
// "the switch expression does not cover all possible input values"
boolean result = switch (ternaryBool) {
case TRUE -> true;
// no case for `FALSE`
case FILE_NOT_FOUND -> throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
};
Интересным является следующее решение: добавление ветки default, конечно, исправит ошибку, но это не является единственным решением — еще можно добавить case для FALSE.
// compiles without `default` branch because
// all cases for `ternaryBool` are covered
boolean result = switch (ternaryBool) {
case TRUE -> true;
case FALSE -> false;
case FILE_NOT_FOUND -> throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
};
Да, компилятор наконец-то сможет определить, охватываются ли все значения enum, что позволяет не использовать бесполезные значения в default!
Что касается исчерпываемости, я стараюсь избегать ветвей по умолчанию, когда это возможно, предпочитая получать ошибки компиляции, когда что-то меняется.
Хотя, это все же вызывает один вопрос. Что делать, если кто-то возьмет и превратит сумасшедший Bool в кватернионный (с четырьмя значениями) Boolean, добавив четвертое значение? Если вы перекомпилируете switch-выражение для расширенного Bool, то получите ошибку компиляции (т. к. выражение больше не будет исчерпывающим). Чтобы отловить эту проблему, компилятор переходит в ветку default, которая ведет себя так же, как та, которую мы использовали до сих пор, вызывая исключение.
В настоящее время охват всех значений без ветки default работает только для enum, но когда switch в будущих версиях Java станет более мощным, он также сможет работать и с произвольными типами. Если метки case смогут не только проверять равенство, но и проводить сравнения (например _ < 5 -> …) — это позволит охватить все варианты для числовых типов.
Как пользоваться switch в современной Java
До этого мы рассматривали изменения, которые произошли до Java 14. Теперь обсудим то, что было реализовано после.
Паттерны (шаблоны)
Реализация сопоставления с образцом в switch все еще находится в процессе разработки, но есть три аспекта, которые особенно интересны по данной теме.
Паттерны типов
На момент написания статьи Java поддерживает только паттерны типов (Type Patterns) с паттернами деконструкции для записей (records), предложенными JEP 405. Их уже можно использовать в операторах if и switch:
Object obj = // ...
// работает с Java 16
if (obj instanceof String str)
callStringMethod(str);
else if (obj instanceof Number no)
callNumberMethod(no);
else
callObjectMethod(obj);
// работает (как превью) с JDK 17+
switch (obj) {
case String str -> callStringMethod(str);
case Number no -> callNumberMethod(no);
default -> callObjectMethod(obj);
}
Я думаю, что с такими возможностями switch станет более функциональным и интуитивным за счет того, что:
- более четко выражает намерение выполнить ровно одну ветвь на основе свойств obj
- компилятор проверяет исчерпываемость
- если необходимо вычислить значение, то использование switch в качестве выражения является более кратким
Применение уточнений (Clauses)
Уточнения (ранее — guarded patterns) расширяют возможности паттерна с помощью дополнительных логических проверок. Это может быть представлено следующим образом (синтаксис, придуманный мной):
String str = // ...
String length = switch (str) {
case str.length() > 42 -> "long";
case str.length() > 19 -> "medium";
case str.length() > 1 -> "small";
case null || str.length() == 0 -> "empty";
};
По мере того, как switch становится все более мощным, я предполагаю, что он начнет поглощать части кода, для реализации которых используется if-else-if.
Выводы
Из статьи мы узнали, что Java превращает switch в выражение, наделяя его новыми возможностями:
- теперь один case может соответствовать нескольким меткам
- новая стрелочная форма case … -> … следует синтаксису лямбда-выражений:
- допускаются однострочные операторы или блоки
- предотвращается сквозной переход к следующему case
- теперь все выражение оценивается, как значение, которое затем может быть присвоено переменной или передано, как часть более крупного оператора
- множественное выражение: если целевой тип известен, то все ветки должны ему соответствовать. В противном случае определяется конкретный тип, который соответствует всем веткам
- yield возвращает значение из блока
- для выражения switch, использующее enum, компилятор проверяет охват всех его значений. Если default отсутствует, добавляется ветка, которая вызывает исключение
- если шаблоны станут более функциональными, то они смогут сделать switch предпочтительнее if