Перечисления
Чему вы научитесь:
- объявлять перечисление и разбирать его в
switchбез веткиdefault; - задавать вариантам сырые значения и находить вариант по номеру;
- хранить в варианте связанные значения и доставать их через
switchиif case; - добавлять перечислению методы и объяснять, почему
Optionalтоже перечисление.

Представьте меню маленького кафе. В нём всего две строки: чай и кофе. Других напитков здесь нет, и заказать лимонад не получится: такой строки в меню нет. Официант записывает на чек одну из строк меню, а если нужно, и детали заказа, например размер L.
В программе заказ можно хранить строкой: "coffee". Но строка примет и "cofee" с опечаткой, и "lemonade", которого в меню нет. Компилятор не заметит ни того, ни другого: для него это обычные строки.
Чтобы компилятор знал меню целиком, в Swift есть перечисления:
enum Drink {
case tea
case coffee
}
let order = Drink.coffee
print(order) // coffeeДавайте разберём программу по строкам:
enum Drinkобъявляет новый типDrink— меню напитков.case teaиcase coffeeперечисляют значения этого типа. Других значений уDrinkне бывает.Drink.coffee— имя типа, точка и одно из значений. Оно попадает в константуorderтипаDrink.print(order)печатает имя значения:coffee.
Теперь опечатку поймает компилятор. Строка let wrong = Drink.cofee не скомпилируется:
error: type 'Drink' has no member 'cofee'Сообщение говорит: в типе Drink нет ничего с именем cofee. Так же не пройдёт и Drink.lemonade.
Объявление перечисления
Перечисление — тип с заранее заданным списком значений. Каждое значение из этого списка называют вариантом перечисления. Объявление состоит из трёх частей:
enum— ключевое слово, с которого начинается объявление;Drink— имя нового типа; его пишут с заглавной буквы, какIntиString;case tea— вариант; имена вариантов пишут со строчной буквы.
Варианты можно перечислить и в одной строке через запятую: case tea, coffee. Это то же самое объявление, только короче.
Точка вместо имени типа
Имя типа перед точкой можно не писать, если Swift уже знает, что нужен Drink. Например, в var next: Drink = .tea тип указан у переменной, и .tea означает Drink.tea. Так же точка работает в ветках switch и в аргументах функции, у которой параметр типа Drink.
switch без default
Перечисление удобно разбирать в switch: у каждого варианта своя ветка. Посчитаем цену напитка:
func price(of drink: Drink) -> Int {
switch drink {
case .tea:
return 2
case .coffee:
return 3
}
}
print(price(of: .tea)) // 2
print(price(of: order)) // 3Давайте пройдём по программе:
- У параметра
drinkтипDrink, поэтому в ветках хватает точки:case .tea:. - Ветки
defaultнет. УDrinkровно два варианта, и для каждого есть ветка, поэтомуswitchисчерпывающий. - В вызове
price(of: .tea)точка тоже работает: параметр объявлен с типомDrink. - Второй вызов получает
orderиз первого примера, то есть кофе, и возвращает3.
В теме об условиях switch по Int без default не компилировался: покрывают ли значения и диапазоны в ветках все числа, компилятор не проверяет. А варианты перечисления он знает все до одного и проверяет каждый. Допишите в меню case cocoa, и функция price перестанет компилироваться:
error: switch must be exhaustiveНиже компилятор подсказывает add missing case: '.cocoa', то есть «добавьте ветку для .cocoa».
Этот вопрос часто задают на собеседовании: почему в switch по перечислению лучше обходиться без default? Ответ:
- без
defaultкомпилятор сам проверяет, что у каждого варианта есть ветка; - когда в перечисление добавят вариант, компилятор укажет на каждый
switch, который его не учёл; - с
defaultновый вариант молча попал бы в общую ветку, и какао продавалось бы по цене изdefault.
А если написать default, когда все варианты уже разобраны, компилятор предупредит: default will never be executed, то есть «default никогда не выполнится».
Все варианты: allCases
Иногда нужен весь список вариантов, например чтобы напечатать меню. Для этого после имени перечисления через двоеточие пишут CaseIterable:
enum Size: CaseIterable {
case small, medium, large
}
print(Size.allCases.count) // 3
for size in Size.allCases {
print(size, terminator: " ") // small medium large
}: CaseIterable просит Swift собрать все варианты в массив allCases. Варианты лежат в нём в том же порядке, что и в объявлении. С этим массивом работают как с любым другим: count даёт число вариантов, а for-in перебирает их по очереди.
Здесь картинка с меню неточна. Меню в кафе можно перепечатать хоть завтра, и в нём появится лимонад. А варианты перечисления записаны в коде: пока программа работает, новый вариант появиться не может.
Частая ошибка
Вариант выбрали через точку, но тип нигде не указали:
let next = .tea // ✗error: reference to member 'tea' cannot be resolved without a contextual typeСообщение говорит: по одной точке Swift не понимает, о каком типе речь. Вариант с именем tea может найтись в разных перечислениях, а у константы next тип не указан. Напишите тип целиком, let next = Drink.tea, или укажите его у константы: let next: Drink = .tea.
Главное
- Перечисление — тип с заранее заданным списком вариантов:
enum Drink { case tea, coffee }. - Вариант пишут с именем типа,
Drink.coffee, или с одной точкой,.coffee, когда тип уже известен. switchпо перечислению обходится безdefault: компилятор сам проверяет, что разобран каждый вариант.: CaseIterableпосле имени перечисления даёт массив всех вариантовallCasesв порядке объявления.