Перейти к содержимому

Лекция 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.north
heading = .east // тип уже известен — можно опустить имя enum
print(heading) // east

Сравнение с Python:

from enum import Enum
class Direction(Enum):
NORTH = 1
SOUTH = 2
EAST = 3
WEST = 4
heading = Direction.NORTH
print(heading) # Direction.NORTH
print(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) // 2

4.2. Строковые raw values

При String raw value по умолчанию равен имени варианта:

enum Color: String {
case red // "red"
case green // "green"
case blue // "blue"
case custom = "пользовательский"
}
print(Color.red.rawValue) // red
print(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.80665

5. Инициализация из 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") // Установлен уровень: info
configure(level: "verbose") // Неизвестный уровень логирования: verbose

6. 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.red
print(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.off
s.toggle()
print(s) // on

8. Рекурсивные перечисления: indirect

Ключевое слово indirect позволяет варианту ссылаться на тот же enum. Это полезно для древовидных структур:

indirect enum ArithExpr {
case number(Double)
case addition(ArithExpr, ArithExpr)
case multiplication(ArithExpr, ArithExpr)
}
// Выражение: (2 + 3) * 4
let 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 5

12. 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 enumPython Enum
Ассоциированные значенияДа — каждый case хранит разные данныеНет — только фиксированное значение
Методы и свойстваДаДа (обычные методы класса)
Raw valuesString, Int, Double и др.Любой тип (через .value)
Pattern matchingПолноценный: switch, if case, for casematch/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) # red
print(Color("red")) # Color.RED — аналог init?(rawValue:)
# Перебор
for c in Color:
print(c.name, c.value)

Аналог с ассоциированными значениями в Python невозможен напрямую. Обычно используют dataclass или именованные кортежи:

from dataclasses import dataclass
from typing import Union
@dataclass
class Success:
data: str
status_code: int
@dataclass
class Failure:
error: str
status_code: int
@dataclass
class 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. Вопросы для самопроверки

  1. Чем перечисления Swift принципиально отличаются от Enum в Python?
  2. Что такое raw value? Какие типы могут быть raw value?
  3. Что возвращает init?(rawValue:) и почему он возвращает Optional?
  4. В чём разница между raw values и associated values? Можно ли использовать оба в одном enum?
  5. Что делает ключевое слово indirect? Когда оно необходимо?
  6. Как извлечь associated value из enum без полного switch?
  7. Объясните разницу между if case let и for case let.
  8. Для чего используется where в pattern matching?
  9. Почему enum хорошо подходит для моделирования конечных автоматов?
  10. Как бы вы реализовали аналог Swift enum с associated values в Python? Какие недостатки у такого подхода?

17. Итоги

В этой лекции мы изучили:

  • Базовый синтаксис enum — определение типов с конечным набором вариантов.
  • Raw values — привязку значений Int, String, Double с автоматическим присвоением.
  • Инициализацию из raw value — failable-инициализатор init?(rawValue:).
  • Associated values — хранение разнородных данных в каждом case.
  • Методы и свойства — enum как полноценный тип со своим поведением.
  • Рекурсивные перечисленияindirect для древовидных структур.
  • Pattern matchingswitch, if case let, for case let, where для извлечения и фильтрации данных.
  • Моделирование состояний — использование enum для конечных автоматов с контролем переходов.

В следующей лекции мы рассмотрим протоколы (Protocols) — механизм абстракции в Swift, заменяющий множественное наследование и интерфейсы.