Лекция 9. Перечисления (Enumerations) и Pattern Matching
1. Введение
Перечисления (enum) — один из самых мощных инструментов системы типов Swift. В отличие от Python, где Enum — это, по сути, набор именованных констант, перечисления в Swift могут хранить ассоциированные данные, содержать методы и вычисляемые свойства, быть рекурсивными и полноценно участвовать в pattern matching. В этой лекции мы рассмотрим все возможности enum в Swift, подробно разберём механизм сопоставления с образцом и сравним подходы с Python.
2. Базовый синтаксис: enum с case
Перечисление определяет тип с конечным набором вариантов:
enum Direction { case north case south case east case west}Можно записать варианты через запятую в одной строке:
enum Season { case spring, summer, autumn, winter}Создание и использование:
var heading = Direction.northheading = .east // тип уже известен — можно опустить имя enum
print(heading) // eastСравнение с Python:
from enum import Enum
class Direction(Enum): NORTH = 1 SOUTH = 2 EAST = 3 WEST = 4
heading = Direction.NORTHprint(heading) # Direction.NORTHprint(heading.value) # 1Ключевое отличие: в Python каждому варианту необходимо присвоить значение. В Swift варианты case являются самостоятельными значениями типа и не обязаны иметь числовой или строковый эквивалент.
3. Использование enum в switch
switch — основной способ работы с перечислениями. Swift требует исчерпывающей обработки всех вариантов:
enum Planet { case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune}
let planet = Planet.earth
switch planet {case .mercury: print("Ближайшая к Солнцу")case .earth: print("Наш дом")case .mars: print("Красная планета")default: print("Другая планета")}// Наш домЕсли перечислены все варианты, default не нужен. Компилятор проверяет полноту — при добавлении нового case все switch-выражения без default вызовут ошибку компиляции. Это важное преимущество перед Python, где забытый вариант обнаружится только в рантайме.
4. Raw values: String, Int, Double
Перечислению можно задать сырой тип (raw value). Swift автоматически присваивает значения:
4.1. Целочисленные raw values
enum StatusCode: Int { case ok = 200 case notFound = 404 case serverError = 500}
print(StatusCode.ok.rawValue) // 200При Int без явного значения нумерация начинается с 0:
enum Weekday: Int { case monday // 0 case tuesday // 1 case wednesday // 2 case thursday // 3 case friday // 4 case saturday // 5 case sunday // 6}
print(Weekday.wednesday.rawValue) // 24.2. Строковые raw values
При String raw value по умолчанию равен имени варианта:
enum Color: String { case red // "red" case green // "green" case blue // "blue" case custom = "пользовательский"}
print(Color.red.rawValue) // redprint(Color.custom.rawValue) // пользовательский4.3. Raw values с Double
enum PhysicalConstant: Double { case pi = 3.14159265358979 case e = 2.71828182845904 case g = 9.80665}
print(PhysicalConstant.g.rawValue) // 9.806655. Инициализация из raw value: init?(rawValue:)
Компилятор автоматически создаёт failable-инициализатор для enum с raw values:
let code = StatusCode(rawValue: 404)print(code) // Optional(StatusCode.notFound)
let unknown = StatusCode(rawValue: 999)print(unknown) // nilПрактический пример — парсинг конфигурации:
enum LogLevel: String { case debug, info, warning, error}
func configure(level str: String) { guard let level = LogLevel(rawValue: str) else { print("Неизвестный уровень логирования: \(str)") return } print("Установлен уровень: \(level)")}
configure(level: "info") // Установлен уровень: infoconfigure(level: "verbose") // Неизвестный уровень логирования: verbose6. Associated values — ассоциированные значения
Это главная сила перечислений Swift. Каждый case может хранить разные данные разных типов:
enum Barcode { case upc(Int, Int, Int, Int) case qr(String)}
var productCode = Barcode.upc(8, 85909, 51226, 3)productCode = .qr("ABCDEFGHIJ")Важно: raw values и associated values несовместимы в одном enum. Raw values одинаковы для всех экземпляров данного case, а associated values задаются при создании и могут различаться.
6.1. Пример: сетевой ответ
enum NetworkResponse { case success(data: String, statusCode: Int) case failure(error: String, statusCode: Int) case noConnection}
let response = NetworkResponse.success(data: "{\"user\": \"Анна\"}", statusCode: 200)
switch response {case .success(let data, let statusCode): print("Код \(statusCode): \(data)")case .failure(let error, let statusCode): print("Ошибка \(statusCode): \(error)")case .noConnection: print("Нет подключения к сети")}// Код 200: {"user": "Анна"}6.2. Пример: Result-подобный enum
Обобщённый тип для представления результата операции:
enum Result<Success, Failure> { case success(Success) case failure(Failure)}
func divide(_ a: Double, by b: Double) -> Result<Double, String> { if b == 0 { return .failure("Деление на ноль") } return .success(a / b)}
let result = divide(10, by: 3)switch result {case .success(let value): print("Результат: \(value)")case .failure(let error): print("Ошибка: \(error)")}// Результат: 3.3333333333333335Примечание: в стандартной библиотеке Swift уже есть тип
Result<Success, Failure: Error>. Здесь мы создали упрощённую версию для демонстрации.
7. Методы и вычисляемые свойства в enum
Перечисления в Swift — полноценные типы. Они могут содержать методы и вычисляемые свойства:
enum TrafficLight { case red, yellow, green
var description: String { switch self { case .red: return "Стоп" case .yellow: return "Внимание" case .green: return "Можно ехать" } }
func next() -> TrafficLight { switch self { case .red: return .green case .yellow: return .red case .green: return .yellow } }}
var light = TrafficLight.redprint(light.description) // Стопlight = light.next()print(light.description) // Можно ехатьlight = light.next()print(light.description) // ВниманиеДля mutating-методов, изменяющих self:
enum Switch { case on, off
mutating func toggle() { self = (self == .on) ? .off : .on }}
var s = Switch.offs.toggle()print(s) // on8. Рекурсивные перечисления: indirect
Ключевое слово indirect позволяет варианту ссылаться на тот же enum. Это полезно для древовидных структур:
indirect enum ArithExpr { case number(Double) case addition(ArithExpr, ArithExpr) case multiplication(ArithExpr, ArithExpr)}
// Выражение: (2 + 3) * 4let two = ArithExpr.number(2)let three = ArithExpr.number(3)let four = ArithExpr.number(4)let sum = ArithExpr.addition(two, three)let product = ArithExpr.multiplication(sum, four)
func evaluate(_ expr: ArithExpr) -> Double { switch expr { case .number(let value): return value case .addition(let left, let right): return evaluate(left) + evaluate(right) case .multiplication(let left, let right): return evaluate(left) * evaluate(right) }}
print(evaluate(product)) // 20.0Без indirect компилятор не сможет определить размер типа, так как значение может содержать вложенные экземпляры того же enum бесконечной глубины. indirect указывает Swift хранить ассоциированные данные через ссылку (в куче), а не напрямую (в стеке).
Можно пометить indirect весь enum вместо отдельных case:
indirect enum JSON { case null case bool(Bool) case number(Double) case string(String) case array([JSON]) case object([String: JSON])}9. Pattern matching: switch и case let
Мы уже видели извлечение associated values в switch. Рассмотрим подробнее синтаксис:
enum Shape { case circle(radius: Double) case rectangle(width: Double, height: Double) case triangle(base: Double, height: Double)}
let shape = Shape.rectangle(width: 5, height: 3)
switch shape {case .circle(let r): print("Круг с радиусом \(r), площадь: \(Double.pi * r * r)")case .rectangle(let w, let h): print("Прямоугольник \(w)×\(h), площадь: \(w * h)")case .triangle(let b, let h): print("Треугольник, площадь: \(b * h / 2)")}// Прямоугольник 5.0×3.0, площадь: 15.0Если все значения извлекаются через let, можно вынести let перед именем case:
case let .rectangle(w, h): print("\(w) × \(h)")10. if case let — проверка одного варианта
Когда нужно проверить только один конкретный case без полного switch:
let response = NetworkResponse.success(data: "OK", statusCode: 200)
if case .success(let data, let code) = response { print("Успех (\(code)): \(data)")}// Успех (200): OKАналог с guard:
func handleResponse(_ resp: NetworkResponse) { guard case .success(let data, _) = resp else { print("Запрос не удался") return } print("Данные: \(data)")}11. for case let — фильтрация в цикле
Позволяет перебирать только элементы, соответствующие заданному образцу:
let responses: [NetworkResponse] = [ .success(data: "Пользователь 1", statusCode: 200), .failure(error: "Не найден", statusCode: 404), .success(data: "Пользователь 2", statusCode: 200), .noConnection, .failure(error: "Таймаут", statusCode: 408),]
print("Успешные ответы:")for case .success(let data, let code) in responses { print(" [\(code)] \(data)")}// Успешные ответы:// [200] Пользователь 1// [200] Пользователь 2Работает и с Optional:
let numbers: [Int?] = [1, nil, 3, nil, 5]
for case let value? in numbers { print(value, terminator: " ")}// 1 3 512. where-условия в pattern matching
where добавляет дополнительные условия к совпадению:
enum Temperature { case celsius(Double) case fahrenheit(Double)}
let readings: [Temperature] = [ .celsius(36.6), .celsius(38.5), .fahrenheit(98.6), .celsius(40.1), .fahrenheit(104.0),]
for case .celsius(let temp) in readings where temp > 37.0 { print("Повышенная температура: \(temp)°C")}// Повышенная температура: 38.5°C// Повышенная температура: 40.1°CВ switch where работает так же:
let point = (3, -2)
switch point {case let (x, y) where x == y: print("На диагонали y = x")case let (x, y) where x == -y: print("На диагонали y = -x")case let (x, _) where x > 0: print("Правая полуплоскость")default: print("Другая точка")}// Правая полуплоскость13. Моделирование состояний (State Machine)
Перечисления идеально подходят для моделирования конечных автоматов:
enum OrderState { case created case confirmed(orderNumber: Int) case shipped(trackingId: String) case delivered(date: String) case cancelled(reason: String)}
struct Order { var id: Int var state: OrderState
mutating func confirm(orderNumber: Int) { guard case .created = state else { print("Нельзя подтвердить заказ в состоянии \(state)") return } state = .confirmed(orderNumber: orderNumber) }
mutating func ship(trackingId: String) { guard case .confirmed = state else { print("Нельзя отправить — заказ не подтверждён") return } state = .shipped(trackingId: trackingId) }
mutating func deliver(date: String) { guard case .shipped = state else { print("Нельзя доставить — заказ не отправлен") return } state = .delivered(date: date) }
func describe() -> String { switch state { case .created: return "Заказ #\(id): создан" case .confirmed(let num): return "Заказ #\(id): подтверждён (номер \(num))" case .shipped(let tracking): return "Заказ #\(id): отправлен (\(tracking))" case .delivered(let date): return "Заказ #\(id): доставлен \(date)" case .cancelled(let reason): return "Заказ #\(id): отменён — \(reason)" } }}
var order = Order(id: 1, state: .created)print(order.describe()) // Заказ #1: созданorder.confirm(orderNumber: 5001)print(order.describe()) // Заказ #1: подтверждён (номер 5001)order.ship(trackingId: "RU123456789")print(order.describe()) // Заказ #1: отправлен (RU123456789)order.deliver(date: "2026-02-14")print(order.describe()) // Заказ #1: доставлен 2026-02-14Каждый переход между состояниями контролируется — невалидные переходы обрабатываются явно. Компилятор проверяет полноту switch, что исключает пропущенные состояния.
14. Сравнение с Python Enum
| Аспект | Swift enum | Python Enum |
|---|---|---|
| Ассоциированные значения | Да — каждый case хранит разные данные | Нет — только фиксированное значение |
| Методы и свойства | Да | Да (обычные методы класса) |
| Raw values | String, Int, Double и др. | Любой тип (через .value) |
| Pattern matching | Полноценный: switch, if case, for case | match/case (с Python 3.10) |
| Рекурсивные типы | indirect enum | Не поддерживается |
| Исчерпывающая проверка | Компилятор требует обработки всех case | Нет проверки |
| Обобщённые типы | Да (enum Result<T, E>) | Нет (можно имитировать) |
Пример Python Enum:
from enum import Enum
class Color(Enum): RED = "red" GREEN = "green" BLUE = "blue"
# Доступ к значениюprint(Color.RED.value) # redprint(Color("red")) # Color.RED — аналог init?(rawValue:)
# Переборfor c in Color: print(c.name, c.value)Аналог с ассоциированными значениями в Python невозможен напрямую. Обычно используют dataclass или именованные кортежи:
from dataclasses import dataclassfrom typing import Union
@dataclassclass Success: data: str status_code: int
@dataclassclass Failure: error: str status_code: int
@dataclassclass NoConnection: pass
NetworkResponse = Union[Success, Failure, NoConnection]В Swift всё это выражается одним лаконичным enum NetworkResponse с ассоциированными значениями и полной проверкой компилятором.
15. Упражнения
Упражнение 1. Создайте перечисление Coin с вариантами kopeck(Int), ruble(Int), dollar(Int) (ассоциированное значение — номинал). Напишите функцию totalInRubles(_ coins: [Coin]) -> Double, которая считает общую сумму в рублях (курс доллара = 90.0).
Упражнение 2. Создайте enum MathOperation с case add(Double, Double), subtract(Double, Double), multiply(Double, Double), divide(Double, Double). Напишите функцию calculate(_ op: MathOperation) -> Result<Double, String>, возвращающую результат или ошибку при делении на ноль (используйте собственный Result).
Упражнение 3. Создайте indirect enum FileSystemItem с вариантами file(name: String, size: Int) и folder(name: String, contents: [FileSystemItem]). Напишите функцию totalSize(_ item: FileSystemItem) -> Int, рекурсивно вычисляющую суммарный размер.
Упражнение 4. Дан массив значений enum Weather:
enum Weather { case sunny(tempC: Double) case rainy(tempC: Double, mmPrecipitation: Double) case snowy(tempC: Double, cmSnow: Double) case cloudy}С помощью for case let ... where выведите только дождливые дни с осадками более 10 мм.
Упражнение 5. Создайте конечный автомат для состояния пользователя: guest, loggedIn(username: String), banned(reason: String). Реализуйте struct UserSession с методами login(username:), ban(reason:), logout(), проверяющими допустимость перехода через guard case.
16. Вопросы для самопроверки
- Чем перечисления Swift принципиально отличаются от
Enumв Python? - Что такое raw value? Какие типы могут быть raw value?
- Что возвращает
init?(rawValue:)и почему он возвращает Optional? - В чём разница между raw values и associated values? Можно ли использовать оба в одном enum?
- Что делает ключевое слово
indirect? Когда оно необходимо? - Как извлечь associated value из enum без полного
switch? - Объясните разницу между
if case letиfor case let. - Для чего используется
whereв pattern matching? - Почему enum хорошо подходит для моделирования конечных автоматов?
- Как бы вы реализовали аналог Swift enum с associated values в Python? Какие недостатки у такого подхода?
17. Итоги
В этой лекции мы изучили:
- Базовый синтаксис enum — определение типов с конечным набором вариантов.
- Raw values — привязку значений
Int,String,Doubleс автоматическим присвоением. - Инициализацию из raw value — failable-инициализатор
init?(rawValue:). - Associated values — хранение разнородных данных в каждом case.
- Методы и свойства — enum как полноценный тип со своим поведением.
- Рекурсивные перечисления —
indirectдля древовидных структур. - Pattern matching —
switch,if case let,for case let,whereдля извлечения и фильтрации данных. - Моделирование состояний — использование enum для конечных автоматов с контролем переходов.
В следующей лекции мы рассмотрим протоколы (Protocols) — механизм абстракции в Swift, заменяющий множественное наследование и интерфейсы.