Зачем нужен закрытый список вариантов
Представьте, что вы храните статус заказа строкой: "new", "paid", "shipped". Компилятор не мешает написать "paidd" с опечаткой или сравнить статус с числом — строка ничего не проверяет, любое значение для неё законно. А по смыслу вариантов на самом деле конечное число, и хочется, чтобы компилятор сам следил: значение — одно из перечисленных, и никакое другое.
Для этого в Swift есть перечисления — тип enum, который описывает закрытый список вариантов, называемых кейсами (case). Переменная такого типа не может принять значение вне списка, а switch по перечислению компилятор проверяет на полноту: забыли разобрать вариант — ошибка компиляции, а не сюрприз в продакшене.
Объявление и простой enum
enum OrderStatus {
case new
case paid
case shipped
case cancelled
}
var status = OrderStatus.new
status = .paid // тип уже известен, точку можно не повторять
Кейсы можно перечислить в одну строку через запятую: case new, paid, shipped, cancelled. Как только компилятор знает тип переменной, писать полное имя OrderStatus.paid не обязательно — короткая форма .paid означает то же самое.
switch по перечислению
Главный способ работать со значением перечисления — switch. Он обязан разобрать все кейсы:
func describe(_ status: OrderStatus) -> String {
switch status {
case .new:
return "Заказ создан"
case .paid:
return "Оплата получена"
case .shipped:
return "Заказ отправлен"
case .cancelled:
return "Заказ отменён"
}
}
Уберите один case — компилятор откажется собирать код, пока вы не разберёте его тоже (или не добавите default). Это и есть исчерпывающий switch: перечисление гарантирует, что вы не забыли ни одну ветку логики.
Raw values: значение под капотом
Иногда у кейса удобно иметь готовое представление — число или строку, которые с ним хранятся всегда. Это называется raw value:
enum Direction: Int {
case north = 0
case east = 1
case south = 2
case west = 3
}
enum Weekday: String {
case monday = "Понедельник"
case tuesday = "Вторник"
}
let code = Direction.east.rawValue // 1
let label = Weekday.monday.rawValue // "Понедельник"
Если тип raw value — Int, следующий кейс без явного значения продолжает счёт с предыдущего плюс один, а первый по умолчанию начинается с нуля. Для String без явного значения имя кейса становится строкой самим по себе.
Обратная операция — получить кейс по значению — возвращает опционал, потому что значения может не быть ни у одного кейса:
let parsed = Direction(rawValue: 2) // Optional(.south)
let bad = Direction(rawValue: 9) // nil
Важно не путать rawValue с чем-то, что перечисление хранит дополнительно к кейсу: это именно представление самого кейса, одно и то же для всех значений этого варианта.
Associated values: данные разной формы для разных кейсов
Raw value одинаков для всех значений одного кейса. Но иногда каждому конкретному значению нужны свои, отдельные данные — и у разных кейсов эти данные разной формы. Для этого у кейса можно объявить associated values, как у функции — параметры:
enum PaymentMethod {
case cash
case card(number: String, expiry: String)
case transfer(account: String)
}
let method = PaymentMethod.card(number: "1234", expiry: "12/27")
Достать эти данные можно через switch с привязкой значений (value binding):
switch method {
case .cash:
print("Наличными")
case .card(let number, let expiry):
print("Карта \(number), до \(expiry)")
case .transfer(let account):
print("Перевод на \(account)")
}
Ключевая разница с raw value: у raw value значение прибито к кейсу заранее и одно на всех (Direction.east всегда 1), а associated values задаются в момент создания конкретного значения и у двух значений одного кейса могут отличаться (card("1234", ...) и card("5678", ...) — оба .card, но с разными числами внутри). У перечисления может быть либо raw value, либо associated values у кейсов — не то и другое сразу.
CaseIterable: перебрать все варианты
Когда у перечисления нет associated values, часто хочется получить список всех кейсов сразу — например, для меню выбора. Для этого перечисление объявляют соответствующим протоколу CaseIterable, и компилятор сам добавляет статическое свойство allCases:
enum Direction: Int, CaseIterable {
case north = 0
case east = 1
case south = 2
case west = 3
}
for d in Direction.allCases {
print(d)
}
print(Direction.allCases.count) // 4
Подробно про протоколы и как они устроены будет отдельный разговор дальше в курсе — здесь достаточно знать, что CaseIterable просто просят у компилятора, дописав его через запятую после типа raw value (или сразу после имени, если raw value нет).
Методы и вычисляемые свойства у enum
Перечисление — полноценный тип, у него могут быть свои методы и вычисляемые свойства, как у структуры:
enum Direction: Int, CaseIterable {
case north = 0, east = 1, south = 2, west = 3
var opposite: Direction {
switch self {
case .north: return .south
case .south: return .north
case .east: return .west
case .west: return .east
}
}
}
let back = Direction.north.opposite // .south
indirect: перечисление, которое ссылается само на себя
Иногда значение перечисления по смыслу должно содержать другое значение того же перечисления — например, узел дерева или математическое выражение. Просто так это не скомпилируется: размер значения enum не может зависеть от самого себя. Ключевое слово indirect просит компилятор хранить такой кейс через ссылку, а не встраивать напрямую:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
}
let expr = ArithmeticExpression.addition(.number(2), .number(3))
indirect можно поставить перед всем enum, если так устроены несколько кейсов, или только перед конкретным кейсом, если рекурсивный только он. На этом уроке достаточно понимать идею — подробно с рекурсивными структурами данных вы встретитесь позже.
Частые ошибки
- Ждать, что у перечисления одновременно будут и raw value, и associated values у кейсов — так нельзя, это взаимоисключающие способы задать данные.
- Путать
rawValueс associated value:Direction.east.rawValue— это заранее заданное число1, одно и то же для всех.east, а не что-то, что вы передали при создании. - Забывать, что инициализатор
Тип(rawValue:)возвращает опционал: значения с таким raw value может не быть ни у одного кейса, поэтому результат нужно разворачивать. - Оставлять
switchнеполным и добавлятьdefaultпросто чтобы компилятор замолчал — так теряется главная польза перечисления: гарантия, что новый кейс не забудут разобрать нигде. - Пытаться сравнить значения с associated values оператором
==без явной поддержки сравнения — компилятор не выводит его сам, если данные внутри кейсов не позволяют это сделать автоматически.
Резюме
enumзадаёт закрытый список вариантов — кейсов; значение может быть только одним из них.switchпо перечислению исчерпывающий: компилятор заставляет разобрать каждый кейс.- Raw value — одно и то же значение (число или строка), навсегда закреплённое за кейсом; получить кейс по значению можно через
Тип(rawValue:), результат — опционал. - Associated values — данные, которые задаются отдельно для каждого конкретного значения кейса; достаются через
switchс привязкой. CaseIterableдаёт готовый список всех кейсов черезallCases.- У перечисления могут быть свои методы и вычисляемые свойства, как у структуры.
indirectпозволяет кейсу ссылаться на значение того же перечисления.
Проверьте себя
4 вопроса
Направление по компасу
Есть перечисление направлений света с raw value типа Int: north = 0, east = 1, south = 2, west = 3 — назовите его Direction.
Реализуйте функцию opposite(_ direction: Direction) -> Direction, которая возвращает противоположное направление (север и юг меняются местами, восток и запад — тоже).
Также реализуйте функцию direction(fromDegrees degrees: Int) -> Direction?, которая по числу градусов возвращает направление: 0 — север, 90 — восток, 180 — юг, 270 — запад. Для любого другого значения градусов верните nil.
Калькулятор фигур
Объявите перечисление Shape с двумя кейсами и associated values: circle(radius: Double) и rectangle(width: Double, height: Double).
Реализуйте функцию area(_ shape: Shape) -> Double, которая возвращает площадь фигуры (площадь круга — 3.14159 * radius * radius, площадь прямоугольника — width * height).
Также реализуйте функцию describe(_ shape: Shape) -> String, которая возвращает "круг" для circle и "прямоугольник" для rectangle, независимо от размеров.