Перейти к содержимому
Теория · Перечисления · урок 1 из 8

Перечисления

Чему вы научитесь

Чему вы научитесь:

  • объявлять перечисление и разбирать его в switch без ветки default;
  • задавать вариантам сырые значения и находить вариант по номеру;
  • хранить в варианте связанные значения и доставать их через switch и if case;
  • добавлять перечислению методы и объяснять, почему Optional тоже перечисление.
Доска меню с двумя строками .tea и .coffee и чек size: L, приколотый к .coffee

Представьте меню маленького кафе. В нём всего две строки: чай и кофе. Других напитков здесь нет, и заказать лимонад не получится: такой строки в меню нет. Официант записывает на чек одну из строк меню, а если нужно, и детали заказа, например размер L.

В программе заказ можно хранить строкой: "coffee". Но строка примет и "cofee" с опечаткой, и "lemonade", которого в меню нет. Компилятор не заметит ни того, ни другого: для него это обычные строки.

Чтобы компилятор знал меню целиком, в Swift есть перечисления:

SWIFT
enum Drink {
    case tea
    case coffee
}

let order = Drink.coffee
print(order) // coffee

Давайте разберём программу по строкам:

  1. enum Drink объявляет новый тип Drink — меню напитков.
  2. case tea и case coffee перечисляют значения этого типа. Других значений у Drink не бывает.
  3. Drink.coffee — имя типа, точка и одно из значений. Оно попадает в константу order типа Drink.
  4. print(order) печатает имя значения: coffee.

Теперь опечатку поймает компилятор. Строка let wrong = Drink.cofee не скомпилируется:

TEXT
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: у каждого варианта своя ветка. Посчитаем цену напитка:

SWIFT
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

Давайте пройдём по программе:

  1. У параметра drink тип Drink, поэтому в ветках хватает точки: case .tea:.
  2. Ветки default нет. У Drink ровно два варианта, и для каждого есть ветка, поэтому switch исчерпывающий.
  3. В вызове price(of: .tea) точка тоже работает: параметр объявлен с типом Drink.
  4. Второй вызов получает order из первого примера, то есть кофе, и возвращает 3.

В теме об условиях switch по Int без default не компилировался: покрывают ли значения и диапазоны в ветках все числа, компилятор не проверяет. А варианты перечисления он знает все до одного и проверяет каждый. Допишите в меню case cocoa, и функция price перестанет компилироваться:

TEXT
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:

SWIFT
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 перебирает их по очереди.

Здесь картинка с меню неточна. Меню в кафе можно перепечатать хоть завтра, и в нём появится лимонад. А варианты перечисления записаны в коде: пока программа работает, новый вариант появиться не может.

Частая ошибка

Вариант выбрали через точку, но тип нигде не указали:

SWIFT
let next = .tea // ✗
TEXT
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 в порядке объявления.