В предыдущем примере, в программе
контроля веса, множественный выбор был
реализован при помощи вложенных одна
в другую инструкций if.
Такой подход не всегда удобен,
особенно в том случае, если количество
вариантов хода программы велико.
В языке Delphi
есть инструкция case,
которая позволяет эффективно реализовать
множественный выбор. В общем виде она
записывается следующим образом:
case
Селектор
of
список1:
begin
{
инструкции
1 }
end;
список2:
begin
{
инструкции
2
}
end;
списокN:
begin
{
инструкции N
}
end;
else
begin
{
инструкции
}
end;
end;
где:
• селектор
– выражение, значение
которого определяет дальнейший ход
выполнения программы (т. е. последовательность
инструкций, которая будет выполнена);
• списокN
– список констант.
Если константы представляют собой
диапазон чисел, то вместо списка
можно указать первую и последнюю
константу диапазона, разделив их
двумя точками. Например, список 1, 2, 3,
4, 5, 6 может быть заменен диапазоном
1..6.
Выполняется инструкция
case
следующим образом:
1. Сначала вычисляется значение
выражения-селектора.
2. Значение выражения-селектора
последовательно сравнивается с
константами из списков констант.
3. Если значение выражения совпадает
с константой из списка, то выполняется
соответствующая этому списку группа
инструкций. На этом выполнение
инструкции case завершается.
4. Если значение выражения-селектора
не совпадает ни с одной константой
из всех списков, то выполняется
последовательность инструкций, следующая
за else.
Синтаксис инструкции case
позволяет не писать else
и соответствующую
последовательность инструкций. В этом
случае, если значение выражения не
совпадает ни с одной константой из всех
списков, то выполняется следующая
за case
инструкция программы. На рис. 5.6 приведен
алгоритм, реализуемый инструкцией
case.
Рис. 5.6. Алгоритм, реализуемый инструкцией
Case
Ниже приведены примеры инструкции
case.
case
n_day of
1,2,3,4,5:
day:=’Рабочий день.’;
б:
day: =’Суббота!’;
7:
day:=’Воскресенье!’;
end;
case
n_day of
1..5:
day:=’Рабочий день.’;
6:
day:=’Суббота!’;
7:
day:=’Воскресенье!’;
end;
case
n_day of
6:
day:=’Суббота!’;
7:
day: = ‘Воскресенье!’;
else
day:=’Рабочий день.’;
end;
В качестве примера использования
инструкции case рассмотрим
программу, которая пересчитывает
вес из фунтов в килограммы. Программа
учитывает, что в разных странах
значение фунта разное. Например, в
России фунт равен 409,5 граммов, в Англии
– 453,592 грамма, а в Германии, Дании и
Исландии фунт весит 500 граммов.
В диалоговом окне программы, изображенном
на рис. 5.7, для выбора страны используется
список Страна.
Рис. 5.7. Форма окна программы Пример
использования case
Для выбора названия страны используется
список – компонент ListBox.
Значок компонента ListBox
находится на вкладке Standard.
Добавляется список к форме приложения
точно так же, как и другие компоненты,
например, командная кнопка или поле
редактирования. В табл. 5.6 приведены
свойства компонента ListBox.
Таблица 5.6
Свойства компонента ListBox
Свойство |
Определяет |
Name |
Имя компонента. В программе |
Items |
Элементы списка |
ItemIndex |
Номер выбранного элемента |
Left |
Расстояние от левой границы списка |
Top |
Расстояние от верхней |
Height |
Высоту поля списка |
Width |
Ширину поля списка |
Font |
Шрифт, используемый для |
ParentFont |
Признак наследования свойств шрифта формы |
Наибольший интерес представляют
свойства items
и itemIndex.
Свойство items
содержит элементы списка. Свойство
itemIndex
задает номер выбранного элемента
списка. Если ни один из элементов не
выбран, то значение свойства равно
минус единице. Список может быть
сформирован во время создания формы
или во время работы программы.
Для формирования списка во время
создания формы надо в окне Object
Inspector
выбрать свойство items
и щелкнуть на кнопке запуска редактора
строк (рис. 5.8). В открывшемся диалоговом
окне String
List
Editor
(рис. 5.9) нужно ввести список, набирая
каждый элемент списка в отдельной
строке. После ввода очередного элемента
списка для перехода к новой строке
необходимо нажать клавишу <Enter>.
После ввода последнего элемента клавишу
<Enter>
нажимать не надо. Завершив ввод списка,
следует щелкнуть на кнопке ОК.
Рис.5.8. Кнопка запуска редактора строк
Рис.5.9. Окно редактора строк
В табл. 5.7 перечислены компоненты формы
приложения с указанием их назначения,
а в табл. 5.8 приведены значения
некоторых свойств этих компонентов,
используемых при работе приложения
Таблица 5.7
Компоненты формы
Компонент |
Назначение |
ListBox1 |
Для выбора страны, для которой надо пересчет |
Edit1 |
Для ввода веса в фунтах |
Label1,Label2, |
Для вывода пояснительного текста о полей |
Label4 |
Для вывода результата пересчета |
Button1 |
Для активизации процедуры пересчета фунтов |
Таблица
5.8
Значения свойств компонентов
Свойство |
Значение |
Form1.Caption |
Пример использования |
Edit1.Text |
|
Label1.Caption |
Выберите страну, введите |
Label2.Caption |
Страна |
Label3.Caption |
Фунтов |
Button1.Caption |
Вычислить |
Процедура пересчета, которая выполняется
в результате щелчка на командной
кнопке Вычислить, умножает вес в
фунтах на коэффициент, равный количеству
килограммов в одном фунте. Значение
коэффициента определяется по номеру
выбранного из списка элемента [3].
В программе
используются две процедуры обработки
событий:
procedure
TForm1.FormCreate(Sender: TObject);
begin
{
ListBox1.items.add(‘Россия’);
ListBox1.items.add(‘Австрия’);
ListBox1.items.add(‘Англия’);
ListBox1.items.add(‘Германия’);
ListBox1.items.add(‘Дания’);
ListBox1.items.add(‘Исландия’);
ListBox1.items.add(‘Италия’);
ListBox1.items.add(‘Нидерланды’);
}
ListBox1.itemindex:=0;
end;
procedure
TForm1.Button1Click(Sender: TObject);
var
funt:real;
// вес в фунтах
kg:real;
// вес в килограммах
k:real;
// коэффициент пересчета
begin
k:=0;
case
ListBox1.ItemIndex of
0:
k:=0.4059; // Россия
1:
k:=0.453592;// Англия
2:
k:=0.56001; // Австрия
3..5,7:k:=0.5;
// Германия, Дания, Исландия, Нидерланды
6:
k:=0.31762; // Италия
end;
funt:=StrToFloat(Edit1.Text);
kg:=k*funt;
label4.caption:=Edit1.Text+
‘ фунт. — ‘
+
FloatToStrF(kg,ffFixed,6,3)+ ‘ кг.’;
end;
Следует обратить внимание на процедуру
обработки события FormCreate,
которое происходит в момент создания
формы (форма создается автоматически
при запуске программы). Эту процедуру
можно использовать для инициализации
переменных программы, в том числе и для
добавления элементов в список. В
приведенном тексте программы инструкции
создания списка закомментированы, т.
к. список был создан при помощи редактора
строк во время создания формы.
Рассмотрим еще один пример использования
инструкции case.
При выводе числовой информации с
поясняющим текстом возникает проблема
согласования выводимого значения
и окончания поясняющего текста.
Например, в зависимости от числового
значения поясняющий текст к денежной
величине может быть: «рубль», «рублей»
или «рубля» (123 рубля, 20 рублей, 121 рубль).
Очевидно, что окончание поясняющего
слова определяется последней цифрой
числа, что отражено в табл. 5.9.
Таблица
5.9
Зависимость окончания текста от
последней цифры числа
Цифра |
Поясняющий текст |
0, 5, 6, 7, 8, 9 |
Рублей |
1 |
Рубль |
2,3,4 |
Рубля |
Приведенное в таблице правило имеет
исключение для чисел, оканчивающихся
на 11, 12, 13, 14. Для них поясняющий текст
должен быть «рублей». Диалоговое окно
программы приведено на рис. 5.10.
Рис. 5.10. Диалоговое окно программы
Поясняющий
текст формирует процедура обработки
события OnKeyPress:
procedure
TForm1.Edit1KeyPress(Sender:TObject;var Key:Char);
var
n
: integer; // число
r
: integer; // остаток от
деления n на 10
text:
string[10]; // формируемый
поясняющий текст
begin
if
Key = chr(VK_RETURN) then
begin
n
:= StrToInt(Edit1.Text);
if
n > 100
then
r:=n mod 100;
if
( r >= 11) and ( r <= 14)
then
text:=’
рублей’
else
begin
r:=
n mod 10;
case
r of
1:
text:=’ рубль’;
2
.. 4: text:=’ рубля’;
else
text:=’ рублей’;
end;
end;
Label2.Caption
:= IntToStr(n)+ text;
end;
end;
Приведём пример полезных процедур, в
которых используется case.
Это процедуры обработки событий
KeyPress. Примеры
процедур запрета ввода недопустимых
символов для программы расчёта дохода
по вкладу. Первая процедура определяет
допустимость ввода символов, составляющих
целое число. Вторая – число вещественного
типа.
//
нажатие в поле срок
procedure
TForm1.Edit3KeyPress(Sender:TObject;var Key:Char);
begin
case
Key of
‘0’
.. ‘9’,#8:
; // цифры и <Backspace>
//перевод
фокуса управления на кнопку вычисления
стоимости
#13:
Button1.SetFocus { Summa(Edit3)} ;
else
Key := Chr(0); // символ
не
отображать
end;
end;
//
нажатие клавиши в поле Сумма
procedure
TForm1.Edit1KeyPress(Sender:TObject;var Key:Char);
begin
case
Key of
‘0’
.. ‘9’, #8 : ; //цифры и <Backspace>
#13:
Form1.Edit2.SetFocus; // <Enter>
‘.’,
‘,’:
begin
if
Key = ‘.’
then
Key:=’,’;
if
Pos(‘,’,Edit1.Text) <> 0
then
Key:=
Chr(0);
end;
else
// все остальные символы запрещены
Key
:= Chr(0);
end;
end;
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
Как мы уже знаем, в исходном коде программы возможно использование вложенных инструкций if. Данный подход не всегда эффективен, особенно тогда, когда число вариантов дальнейшего выполнения программы достаточно велико. Разработчики это предусмотрели и внесли в библиотеку языка цикл case Delphi, способствующую эффективной реализации множественного выбора.
Цикл case Delphi имеет общий вид:
откуда:
- Селектор подразумевает выражение, от значения которого зависит дальнейший вариант выполнения программы (т.е. определяет последовательность инструкций, которой будет руководствоваться программа в процессе своего выполнения);
- Список N обозначает список констант. Если они представлены в виде диапазона чисел, то список можно опустить и вместо него следует подстановка первой и последней констант диапазона, разделенных двумя точками (список 1, 2, 3, 4 можно заменить диапазоном 1..4).
Последовательность выполнения инструкции case такова:
- Вначале производится вычисление значения выражения-селектора;
- Затем происходит последовательное сравнение этого значения с константами, входящими в список констант.
- Далее, если произошло совпадение значения выражения-селектора с одной из констант списка, то осуществляется выполнение соответствующей этому списку группы инструкций, и на этом инструкция caseзавершает свое выполнение.
- Если произошло несовпадение значения выражения ни с одной из констант из всех списков, то производится выполнение последовательности инструкции, следующей за служебным словом else.
Использование синтаксиса инструкции case дает возможность не использовать служебное слово else вместе с соответствующей ему последовательностью инструкций. Тогда, если значения выражения-селектора не равно ни одной из констант из всех списков, то осуществляется выполнение следующей за служебным словом caseинструкции программы. Ниже представлен алгоритм, который реализует цикл case Delphi.
Приведем примеры использования инструкции case в различных ее вариантах.
Пример 1.
Пример 2.
Пример 3.
Теперь рассмотрим уже известную программу перевода веса из фунтов в килограммы на примере инструкции case. Данная программа будет учитывать тот факт, что в разных странах величина одного фунта измеряется по-разному. Например, в Англии 1 фунт = 453,592 гр, в России 1 фунт = 409,5 гр, в Дании, Исландии и Германии 1 фунт = 500 гр. На рисунке ниже представлено диалоговое окно программы, позволяющее выбрать страну на основе списка Страна.
Выбор страны осуществляется при помощи списка — компонента ListBox. Значок этого компонента расположен на вкладке Standard (рис. ниже — указан курсором).
Добавление списка ListBox на форму приложения проводится так же, как и добавление других компонентов: командной кнопки, поля редактирования и т. д. В таблице ниже представлены свойства списка — компонента ListBox.
Больший интерес вызывают свойство Items и свойство Itemindex. Первое из этих свойств включает в себя элементы списка, а второе свойство определяет, как номер был выбран из списка. В случае, когда не выбран ни один элемент из списка, свойство Itemindex принимает значение, равное -1. Что удобно и эффективно, список можно сформировать в процессе создания формы, либо в момент работы программы. Чтобы сформировать список в процессе создания форма, необходимо в окне Object Inspector осуществить выбор свойства Items нажать на кнопку запуска редактора, позволяющим редактировать список строк (рис. ниже).
Появилось диалоговое окно String List Editor (рис. ниже).
В этом окне необходимо ввести список, печатая каждый его элемент в одной строке, нажимая на клавишу Enterпри переходе на новую строку. Когда был введен последний элемент списка, на клавишу Enter не рекомендуется нажимать. Завершив ввод всех элементов списка, нажмите на кнопку OK. В таблице ниже представлены компоненты, располагающиеся на форме приложения нашей программы:
Следующая таблица содержит значений свойств компонентов, которые были использованы в программе:
Процедура перевода веса, выполняющаяся при нажатии на кнопку Вычислить, осуществляет произведение веса в фунтах на коэффициент, который представляет собой количество килограммов, содержащихся в одном фунте.
Листинг программы перевода веса из фунтов в килограммы:
unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Label2: TLabel; Edit1: TEdit; // поле ввода веса в фунтах Button1: TButton; // кнопка Вычислить Label1: TLabel; LabelS: TLabel; ListBox1: TListBox; // список стран Label4: TLabel; // поле вывода рез-та — веса в килограммах procedure FormCreate(Sender: TObject); procedure ButtonlClick(Sender: TObject); private {Private declarations } public { Public declarations } end; var Form1: TForml; implementation {$R *.DFM} procedure TForml.FormCreate(Sender: TObject); begin {ListBox1.items.add('Россия'); ListBox1.items.add('Австрия'); ListBox1.iterns.add('Англия'); ListBox1.items.add('Германия'); ListBox1.iterns.add ('Дания'); ListBoxl.iterns.add('Исландия'); ListBox1.iterns.add ('Италия'); ListBox1.items.add ('Нидерланды'); } ListBox1.itemindex:=0; end; procedure TForm1.ButtonlClick(Sender: TObject); var funt:real; // вес в фунтах kg:real; // вес в килограммах k:real; // коэффициент пересчета begin case ListBoxl.Itemindex of 0: k:=0.4095; // Россия 1: k:=0.453592; // Англия 2:k:=0.56001; // Австрия 3..5,7:k:=0.5; // Германия, Дания, Исландия, Нидерланды 6: k:=0.31762; // Италия end; funt:=StrToFloat(Editl.Text); kg:=k*funt; label4.caption:=Edit1.Text + ' ф. — это ' + FloatToStrF(kg,ffFixed, 6,3) + 'кг.'; end; end.
После загрузки в исходном тексте программы обратите внимание на процедуру FormCreate, обрабатывающую событие, которое возникает во время создания формы (ее создание осуществляется автоматически в процессе запуска программы).
Данная процедура не только инициализирует переменные программы, но и может использоваться для добавления необходимых элементов списка. В листинге данной программы, как Вы можете заметить, инструкции создания списка помещены в комментарии. Это сделано для того, чтобы список был организован редактором строк в процессе создания формы.
Множественный выбор
Условный оператор, как мы видели, решает проблему разделения области задачи на непересекающиеся подмножества, в каждом из которых решение ищется независимо. Базисная форма if … then … else использует два подмножества. Включение elseif позволяет разбивать область на произвольное число частей.
Рис.
7.19.
Выбор из списка
Многие языки программирования предлагают для этих целей другую конструкцию выбора, когда речь идет о выборе из некоторого множества чисел или символьных значений. Рассмотрим приложение с графическим интерфейсом, предоставляющее пользователю право выбора языка интерфейса для дальнейшего общения с пользователем.
Предположим, что в приложении введена целочисленная переменная choice, принимающая значение от 1 до 4, в зависимости от выбора пользователя:
if choice = 1 then ... Выбрать английский в качестве языка интерфейса ... elseif choice = 2 then ... elseif choice = 4 then ... Выбрать русский в качестве языка интерфейса ... else ... Этот вариант не должен встречаться (смотри ниже) ... end
В этом случае множественный выбор доступен в более компактной форме:
inspect choice when 1 then ... Выбрать английский в качестве языка интерфейса ... when 2 then ... when 4 then ... Выбрать русский в качестве языка интерфейса ... else ... end
Сделанные упрощения являются довольно скромными, но они отражают общую идею: если все условия выбора сводятся к форме choice = val_i для одного и того же выражения choice и различных констант val_i,, то нет необходимости в повторении «choice =». Вместо этого можно просто перечислить значения констант в последовательно идущих предложениях when.
Это нотация, используемая в Eiffel. В языках Pascal и Ada есть подобная конструкция с ключевыми словами case … of. В языке С и его последователях (C++, Java, C#) есть оператор switch, не в точности соответствующий нашей нотации, но позволяющий получить ее эквивалент.
Множественный выбор доступен только для выражений определенного типа. В Eiffel его можно использовать только для целых и символов. В обоих случаях:
- применяется простая нотация для выбора значений, такая как 1 для целых и _A_ для символов;
- значения упорядочены. Как следствие, можно создавать интервалы: целых, такие как 1 … 10, символов, такие как ‘A’ …’Z’.
В предложениях when можно использовать интервальную нотацию:
inspect last_character -- Символ введенный пользователем when 'a' .. 'z' then ... Операции для случая ввода буквы латиницы в нижнем регистре ... when 'A' .. 'Z' then ... Операции для случая ввода буквы латиницы в верхнем регистре ... when '0' .. '9' then ... Операции для случая ввода цифры ... else ... Операции для случая, когда ввод не является буквой или цифрой ... end
В предложениях when можно также перечислять несколько значений или несколько интервалов или их смесь, используя запятые в качестве разделителей:
inspect код заказа — при заказе авиабилетов when 'A', 'F', 'P', 'R' then ... Операции при покупке билетов первого класса ... when 'C' .. 'D', 'I .. 'J', then ... Операции при покупке билетов бизнес класса ... when 'B', 'H', 'K .. 'N', 'Q', 'S .. 'W', 'Y' then ... Операции при покупке билетов эконом класса ... else ... Обработка случая для нестандартного кода заказа ... end
С такими возможностями преимущества when нотации становятся ощутимыми. Кроме того, многовариантный выбор позволяет задать содержательное правило «отсутствия пересечений»: никакие значения не могут появляться в двух различных when-ветвях. Компиляторы следят за этим, выдавая ошибку при обнаружении двусмысленности.
В условном операторе if cl then … elseif c2 then … elseif c3 then … end вполне возможно одновременное выполнение нескольких условий cl, c2, c3, …. Явно заданный последовательный характер проверки определяет, что будет выполняться первая ветвь, для которой выполняется условие. Множественный выбор с when требует, чтобы только одно условие было истинным.
Различные варианты условного оператора все же по умолчанию носят исключающий характер. Как определяет его семантика, i-я ветвь соответствует случаю, когда ее условие выполняется и ни одно из условий предыдущих ветвей не выполнилось. Динамически это ведет к непересекающимся случаям. Во множественном выборе отсутствие пересечений задано при проектировании.
Семантическое различие открывает путь к более эффективной реализации множественного выбора. Поскольку условия не должны вычисляться последовательно и благодаря принципу хранимой программы, применима техника, известная как таблица переходов. Строго говоря, она применяется разработчиками компиляторов, а прикладные программисты непосредственно ее не используют, но полезно знать саму идею.
Предположим, необходимо реализовать множественный выбор для случая, когда выбор определяется значениями целых чисел, как в нашем первом примере с выбором языка интерфейса; для общности положим, что выбор определяется значениями n чисел. В условном операторе пришлось бы последовательно проверять, равна ли переменная choice 1, если нет, то равна ли она 2, и так далее, выполняя n сравнений в худшем случае.
Используя дополнительные знания о том, что выбор осуществляется между непересекающимися значениями целочисленного интервала и что программы для каждого случая хранятся в памяти по определенным адресам, можно ввести специальную структуру данных — таблицу переходов, позволяющую сразу же найти нужный элемент для любого из вариантов.
Рис.
7.20.
Использование таблицы переходов для организации множественного выбора
Таблица переходов соответствует массиву — структуре данных высокого уровня, с которой мы вскоре познакомимся. Каждый из ее элементов содержит адрес кода, выполняемого для соответствующей ветви inspect. На рисунке это показано в виде стрелки, указывающей на точку хранения кода. Двойник «стрелки» есть в языках высокого уровня: понятие ссылки или указателя. Трансляция множественного выбора в машинный код проста: выражение choice используется для нахождения нужного элемента в таблице переходов, этот элемент содержит адрес, выполняется код, расположенный по этому адресу.
Важное свойство такой схемы — она не требует последовательных проверок, только доступ к массиву по индексу и переход по полученному адресу. Обе операции выполняются за константное время вне зависимости от числа ветвей.
Предваряя методы, с которыми мы столкнемся при изучении структур данных, мы видим, что этот подход включает компромисс память-время: в стремлении получить выигрыш во времени исполнения мы жертвуем некоторой памятью для хранения таблицы переходов.
Этот пример показывает великолепие техники таблицы переходов, когда все значения принадлежат некоторому интервалу. Для более сложных случаев, как в примере с заказом авиабилетов, преимущества по сравнению с последовательной проверкой не кажутся столь убедительными, но в этом и состоит работа создателей компиляторов, чтобы проектировать лучшую комбинацию обоих подходов для каждого частного случая.
Почувствуй историю
Когда методы реализации влияют на проектирование языка
Исторически операторы множественного выбора появились в языках программирования как следствие таблицы переходов, применяемой в реализации. Первый аналог появился еще в языке Фортран в виде оператора, получившего название «Вычисляемый Goto» в форме GOTO (L1, Ln), CHOICE. Если целочисленное выражение CHOICE имело значение i, то происходил переход по метке Li. Оператор switch языка C и его последователей близок по смыслу. Такие конструкции непосредственно отражают технику таблицы переходов.
Тони Хоар (C.A.R. Hoare) предложил свободную от goto конструкцию case, включенную Виртом в спроектированные им языки Algol W и Pascal. Она и является источником современных форм множественного выбора.
Рис.
7.21.
Хоар (2007)
При проектировании множественного выбора необходимо решить, что делать в случае, когда ни один из указанных вариантов не имеет места. Оператор inspect может включать предложение, обрабатывающее этот случай. Как и для случая условного оператора, это предложение не является обязательным и может быть опущено. В этом случае оба оператора выполняются по-разному.
- В отсутствие предложения else, когда ни одно из условий не выполняется, условный оператор по своему действию эквивалентен пустому оператору.
- Для inspect такая ситуация приведет к ошибке периода выполнения и возникновению исключительной ситуации.
Эта политика вначале может вызывать удивление. Причина в том, что множественный выбор явно перечисляет множество возможных вариантов и действия, предпринимаемые для каждого из них. Если возможны и другие значения, то для их обработки и следует указать предложение. Если такое предложение не включается, то это предполагает, что вы ожидаете появление только значений указанного множества. В случае с выбором языка значением может быть только от 1 до 4. Если эти ожидания нарушены, то ничего не делать — это неправильная идея, так как, вероятнее всего, она приведет к некорректным вычислениям и другим проблемам, ведущим к краху. Гораздо лучше захватить источник проблем в начале его появления и включить исключение.
Это обоснование неприменимо к условному оператору, который последовательно проверяет условие за условием, выполняя некоторые операции, когда условие становится истинным.
7.10. Введение в обработку исключений
«Все счастливые семьи похожи друг на друга, каждая несчастливая семья несчастлива по-своему», — все знают эти первые строчки романа «Анна Каренина». Счастливые выполнения программ все используют одни и те же структуры управления; несчастливые — несчастны по многим различным причинам, называемым исключениями.
Исключения дополняют структуры управления этой лекции, обеспечивая способ управления специальными случаями, не повреждая нормальный поток управления. Поскольку мы не будем нуждаться в исключениях для структур данных и примеров алгоритмов в этой книге, — фактически, они зарезервированы на непредвиденные случаи, — в этом разделе вводятся только основные идеи.
Вы можете рассматривать этот материал как дополнительный и пропустить его (вместе с последующим разделом) при первом чтении.
Роль исключений
Что представляет «специальный» или «исключительный» случай? Мы увидим, что это понятие можно определить вполне строго, но пока будем полагаться на интуицию, считая, что это событие, которое не должно было случиться. Вот примеры некоторых событий, которые могут, каждое по-своему, разрушить блаженство нормального выполнения программы.
- Деление на ноль, или другие арифметические неприятности.
- Попытка создать объект после превышения пределов памяти.
- Вызов void, который мы определили как попытку вызвать метод несуществующим объектом (x.f, где x имеет значение void).
- Нарушение контрактов, если вы включили механизм мониторинга предусловий, постусловий и инвариантов в период выполнения.
- Выполнение оператора, сигнализирующего о возникновении ошибочной ситуации.
Такие события включают исключения. Во всех случаях, за исключением последнего, это будут системные исключения, возникающие по внешним причинам. В последнем случае сама программа явно вызывает исключение, которое относится к программистом определенным исключениям. Это различие иллюстрирует две различные роли обработки исключения.
- Можно использовать обработку исключения как способ последней надежды обработать неожиданное событие, из-за которого нормальный поток управления потерпел неудачу. Нереально защищать каждое деление тестом: а не является ли знаменатель равным нулю? Нереально при каждом создании объекта проверять, а есть ли достаточно памяти. Еще труднее планировать некоторые другие ситуации — отказы аппаратуры, прерывания, инициируемые пользователем. Исключение позволяет программе восстановиться или, по крайней мере, с достоинством закончить свою работу, когда любой из ее операторов был прерван из-за возникновения неожиданного события. «Неожиданное» в данном контексте означает событие, не предусматриваемое при работе этого оператора.
- Случай исключений, определенных программистом, отличен: здесь обработка исключений становится управляющей структурой, которую следует добавить к нашему каталогу.
Точные рамки для обсуждения отказов и исключений
Для проектирования подходящей стратегии использования исключений — в частности, определения того, что является «нормальным», а что «ненормальным», «исключительным», — будем основываться на понятии Проектирования по Контракту, определенном в предыдущих обсуждениях. Мы знаем, что каждая подпрограмма, независимо от ее конкретной реализации, позволяет задать:
- предусловие: требования к вызывающей программе, которые должны быть выполнены непосредственно перед вызовом;
- постусловие: истинное в конце выполнения и отвечающее интересам вызывающей программы;
- инварианты охватывающего класса: общие свойства, необходимые всем методам класса. Этот подход можно расширить и на операции, не являющиеся подпрограммами, например, на сложение с плавающей точкой, на создание объектов и так далее.
В идеальном мире все семьи были бы счастливы, и все операции соответствовали бы своим контрактам. В реальном мире операции по тем или иным причинам не всегда выполняют свои обязательства. Если есть распределитель памяти, но вся доступная память уже распределена, то он не сможет создать объект. Если есть подпрограмма, некорректно запрограммированная, то она не сможет выполнить постусловие. Этих наблюдений достаточно для выработки точных определений.
Определения: Отказ, Исключение, Получатель
Отказ — неспособность операции выполнить свой контракт.
Исключение — отказ одной из операций во время выполнения подпрограммы.
Получатель — подпрограмма, в которой возникло исключение.
«Отказ» является первичным понятием и применяется к операциям любого вида: подпрограммам, но также и к базисным операциям, таким как создание объекта. «Исключение» является производным понятием и применимо только к подпрограммам: в подпрограмме возникает исключение, если она выполняет операцию (базисную или вызов подпрограммы), приводящую к отказу.
Почему исключение — не то же самое, что и отказ? Часто появление исключения приводит к отказу у получателя, но не всегда! Подпрограмма может предусмотреть обработку исключения — код спасения, который попытается исправить положение, повторно запустит программу с лучшим исходом.
Если же подпрограмма не предусмотрела обработку исключений или не смогла исправить положение, то выполнение программы заканчивается отказом. В этом случае она включает исключение у вызывающей программы, у которой снова есть две возможности: отказ или восстановление. В результате исключения распространяются вверх по цепочке вызовов. Если ни одна из подпрограмм в этом процессе не восстановит нормальное выполнение, то программа «завершится с ошибкой».
Мы уже сталкивались с примером исключения, причиной которого был «void-вызов».
Теперь мы знаем, что означает отказ для подпрограмм: быть получателем исключения, не способным восстановить ситуацию.
Реализация множественного выбора в программировании
Множественный выбор (или выбор среди нескольких вариантов) — это одна из самых распространенных операций в программировании. Он позволяет программе принимать решение на основе значения переменной или выражения, выбирая определенный путь выполнения в зависимости от результата.
Существует несколько подходов к реализации множественного выбора в различных языках программирования. В этой статье мы рассмотрим некоторые из них, постараемся разобраться в их особенностях и использовании.
1. Инструкция if-else-if
Наиболее простой способ реализации множественного выбора — использование инструкции if-else-if. Данный подход подразумевает использование нескольких ветвей условий, каждая из которых проверяется последовательно до нахождения соответствующего значения.
if condition1: # код для выполнения при condition1 elif condition2: # код для выполнения при condition2 elif condition3: # код для выполнения при condition3 else: # код для выполнения при отсутствии соответствующего значения
2. Выражение выбора (switch)
В некоторых языках программирования, таких как Java, C++ и C#, есть специальный оператор выбора, называемый switch. Он позволяет выбрать одну из нескольких альтернативных ветвей в зависимости от значения выражения.
switch (variable) { case value1: // код для выполнения при variable == value1 break; case value2: // код для выполнения при variable == value2 break; case value3: // код для выполнения при variable == value3 break; default: // код для выполнения при отсутствии соответствующего значения break; }
3. Структура данных Map
Другой подход к реализации множественного выбора — использование структуры данных Map (ассоциативного массива). В этом случае каждому значению присваивается определенная функция или блок кода, который будет выполнен в случае совпадения.
options = { value1: function1, value2: function2, value3: function3 } result = options.get(variable, default_function) result()
4. Полиморфизм и наследование
В объектно-ориентированных языках программирования множественный выбор может быть реализован с помощью полиморфизма и наследования. В этом случае каждой альтернативе соответствует отдельный класс, а родительский класс содержит виртуальный метод, который переопределяется в каждом дочернем классе.
abstract class Shape { abstract void draw(); } class Circle extends Shape { void draw() { // код для рисования круга } } class Rectangle extends Shape { void draw() { // код для рисования прямоугольника } } Shape shape; if (variable == value1) { shape = new Circle(); } else if (variable == value2) { shape = new Rectangle(); } shape.draw();
В данной статье мы рассмотрели различные способы реализации множественного выбора в программировании. Каждый из них имеет свои особенности и применим в разных ситуациях. Реализация выбора зависит от конкретного языка программирования и его возможностей.
Выбор подходящего метода реализации множественного выбора важен для разработки эффективного и читабельного кода. Необходимо учитывать особенности языка программирования, требования проекта и предпочтения программистов команды.
Правильное использование множественного выбора помогает создать более гибкую и функциональную программу, обеспечивая возможность принимать различные решения в зависимости от входных данных.
Время на прочтение
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