1. Иногда нужно спрятать логику внутрь enum
Когда начинаешь писать программы, очень легко попасть в стиль «всё в одном месте»: прочитал ввод, сделал огромный switch, в каждой ветке что-то распечатал, где-то изменил переменную, где-то ещё раз проверил условие… Работает? Работает. Но через пару дней такой код начинает напоминать ящик с проводами: нужный провод вроде есть, но попробуй найди — и желательно не ударься током.
enum хорош тем, что он не просто ограничивает варианты, он ещё и подсказывает архитектуру: раз у нас есть варианты, значит, у каждого варианта может быть свой смысл. И очень удобно хранить этот смысл рядом с вариантом. Тогда вместо десяти switch-ей по проекту у вас будет один — внутри enum. И если вы добавите новый case, компилятор «подсветит» места, где нужно обновить логику.
Представьте, что enum — это не «табличка с надписями», а «переключатель режимов» в устройстве. Переключатель не только перечисляет режимы, он определяет, что каждый режим делает: как выглядит, как называется, что разрешает, какой следующий режим и так далее.
Computed property в enum: значение «на лету»
Computed property (вычисляемое свойство) — это свойство, у которого нет отдельного места в памяти «под значение». Вместо этого при обращении к нему выполняется код, который вычисляет результат. По сути это «функция без скобочек», но с очень важной договорённостью: свойство должно выглядеть как простое чтение данных и не удивлять пользователя побочными эффектами.
Синтаксис вы уже видели в struct:
var name: String {
return ...
}
В enum всё точно так же. Самый частый паттерн — вычислять что-то в зависимости от текущего case. Например, мы делаем маленькое консольное приложение-библиотеку (мы его будем постепенно «умнеть» дальше по курсу), где пользователь вводит команду. Команда — это enum, а у команды есть «человекочитаемое имя».
import Foundation
enum LibraryCommand {
case help, list, add, exit
var title: String {
switch self {
case .help: return "Справка"
case .list: return "Список книг"
case .add: return "Добавить книгу"
case .exit: return "Выход"
}
}
}
Обратите внимание на приятный эффект: снаружи вам не нужно помнить, как именно «красиво назвать» команду. Вы пишете cmd.title и получаете аккуратный текст. А самое главное — если завтра появится .remove, компилятор заставит вас обновить title, потому что switch обязан быть исчерпывающим.
2. Главный приём: switch self как таблица соответствий
Когда вы впервые видите switch self, он может выглядеть немного странно: «Я что, делаю switch по самому себе?». Да. Именно. self внутри enum — это текущее значение (один из case-ов). И это превращает computed property в очень понятную «табличку правил»: case → результат.
Давайте сделаем свойство, которое возвращает строку подсказки, как пользоваться командой. В нашем учебном CLI это будет мини-справка.
import Foundation
enum LibraryCommand {
case help, list, add, exit
var usage: String {
switch self {
case .help: return "help — показать справку"
case .list: return "list — вывести список книг"
case .add: return "add — добавить книгу (пока без параметров)"
case .exit: return "exit — выйти из программы"
}
}
}
Почему это удобно именно как computed property, а не как отдельная функция usage(for:) где-то снаружи? Потому что тогда usage «принадлежит» команде. Команда сама знает, как она выглядит для пользователя.
Здесь важно почувствовать стиль Swift: «данные и поведение рядом». Мы уже делали это в struct, а enum ничуть не хуже — он такой же полноценный тип.
4. Методы в enum: когда свойств уже тесно
Computed property хорош, когда вы просто возвращаете значение и вам не нужны аргументы. Но как только появляется необходимость принять параметр (например, состояние приложения), сделать действие или вернуть результат вычисления на основе входных данных — вам нужен метод.
Метод внутри enum объявляется точно так же, как метод в struct:
func имя(параметры) -> Возврат { ... }
Вернёмся к нашему LibraryCommand. Представим, что мы хотим выполнить команду и получить текст, который нужно вывести пользователю. Состояние программы пока упрощённое: список книг и флаг, что нужно ли продолжать цикл.
Сначала объявим состояние:
import Foundation
struct LibraryState {
var books: [String] = []
var isRunning: Bool = true
}
Теперь добавим метод run(state:) внутрь enum:
import Foundation
enum LibraryCommand {
case help, list, add, exit
func run(state: inout LibraryState) -> String {
switch self {
case .help:
return "Доступные команды: help, list, add, exit"
case .list:
return state.books.isEmpty ? "Список пуст." : state.books.joined(separator: ", ")
case .add:
state.books.append("Новая книга")
return "Добавлено: Новая книга"
case .exit:
state.isRunning = false
return "Пока!"
}
}
}
Здесь происходит несколько важных вещей, которые стоит проговорить вслух.
Во-первых, enum не просто «определяет варианты». Он стал «объектом поведения»: команда сама умеет выполняться.
Во-вторых, метод принимает inout-параметр state, потому что он меняет состояние (добавляет книгу, завершает цикл). Мы не трактуем это как «передачу по ссылке» в мистическом смысле, но понимаем практично: функция получает право изменить переменную, которую вы передали с &.
В-третьих, switch по self снова становится центром логики. И если добавится новый case, компилятор снова заставит обновить run.
5. mutating методы в enum: когда меняется сам enum
Очень частая модель для enum — это не только «команда», но и «состояние». Например, уровень громкости, статус заказа, режим приложения. В таких моделях удобно менять само значение enum: было .mute, стало .low.
В Swift это делается через mutating-метод. Причина простая: enum — value type. Чтобы изменить значение, вы фактически присваиваете новое значение в self.
Пример «уровень громкости» (мини, но очень показательный):
import Foundation
enum VolumeLevel {
case mute, low, high
mutating func increase() {
switch self {
case .mute: self = .low
case .low: self = .high
case .high: self = .high
}
}
}
И использование:
import Foundation
var volume: VolumeLevel = .mute
volume.increase()
print(volume) // low
Почему это полезно именно для начинающих? Потому что оно очень наглядно объясняет: enum — это значение. Чтобы изменить значение, мы заменяем его целиком на другое допустимое значение. Примерно как переключатель режимов: вы не «подкручиваете» .mute, вы переводите ручку в .low.
6. Мини-приложение: убираем большой switch из main
В реальной жизни начинающий код часто выглядит так: в main огромный switch, который делает всё. Давайте аккуратно соберём цикл команд так, чтобы main был «диспетчером», а не «кухней, бухгалтерией и отделом кадров» одновременно.
Сделаем LibraryCommand с raw values и CaseIterable, чтобы можно было парсить из ввода и показывать справку. Raw values вы уже знаете из прошлой лекции, сегодня мы используем их как удобную «строку команды».
import Foundation
enum LibraryCommand: String, CaseIterable {
case help, list, add, exit
}
Добавим computed property, который красиво печатает команды в подсказке:
import Foundation
enum LibraryCommand: String, CaseIterable {
case help, list, add, exit
static var helpText: String {
allCases.map { $0.rawValue }.joined(separator: ", ")
}
}
Здесь важный момент: helpText — это static computed property. Это не про конкретную команду, это про тип в целом (про весь набор команд). Мы используем allCases, то есть работаем с перечислением вариантов.
Теперь добавим тот самый метод run(state:):
import Foundation
enum LibraryCommand: String, CaseIterable {
case help, list, add, exit
func run(state: inout LibraryState) -> String {
switch self {
case .help:
return "Команды: \(Self.helpText)"
case .list:
return state.books.isEmpty ? "Список пуст." : state.books.joined(separator: ", ")
case .add:
state.books.append("Новая книга")
return "Ок."
case .exit:
state.isRunning = false
return "Выходим."
}
}
}
Обратите внимание: здесь мы разнесли плотные однострочные ветки на несколько строк — так проще читать и расширять код, даже если пример учебный.
И теперь main становится очень компактным и понятным:
import Foundation
var state = LibraryState()
while state.isRunning {
let input = readLine() ?? ""
let cmd = LibraryCommand(rawValue: input) ?? .help
print(cmd.run(state: &state))
}
Вот это ощущение «магии порядка» — не магия, а простая дисциплина: логика команд живёт в enum, а цикл — просто читает ввод, парсит и делегирует выполнение.
7. Полезные нюансы: вывод в строку и выбор между свойством и методом
CustomStringConvertible: делаем print(enum) по‑человечески
Когда вы делаете print(someEnum), Swift должен превратить значение в строку. В простых случаях он печатает имя типа и кейса, но иногда хочется контролировать вывод. Для этого есть протокол CustomStringConvertible и свойство description.
Важно понимать, что print и вообще превращение в строку завязаны на идею «тип может дать строковое представление сам», и одним из стандартных путей как раз является CustomStringConvertible.
Сделаем так, чтобы команды печатались как их raw value, а не как что-то техническое:
import Foundation
enum LibraryCommand: String, CaseIterable, CustomStringConvertible {
case help, list, add, exit
var description: String {
rawValue
}
}
Теперь:
import Foundation
let cmd: LibraryCommand = .list
print(cmd) // list
Почему это удобно? Потому что вы можете печатать команду в сообщениях пользователю, логах или подсказках, и она будет выглядеть одинаково везде. И это снова «логика рядом с типом»: команда сама определяет, как она превращается в текст.
Что выбрать: computed property или метод
Когда у вас появляется возможность «добавить поведение в enum», первая реакция иногда такая: «О, теперь всё сделаю properties!». А потом появляется property, которая читает файл, делает сетевой запрос и ещё заодно меняет состояние… и вы внезапно понимаете, что свойства были не для этого.
Давайте зафиксируем простое правило: computed property хороша, когда она выглядит как «характеристика» и не требует входных данных. Метод хорош, когда есть параметры, действие, или смысл ближе к глаголу.
Небольшая таблица (таблица — не список: глаза скажут спасибо):
| Ситуация | Лучше computed property | Лучше метод |
|---|---|---|
| Нужно вернуть текст/число «по текущему case» | |
возможно, но избыточно |
Нужно принять аргумент (например, ) |
неудобно/невозможно | |
| Нужен побочный эффект (изменить состояние) | лучше избегать | метод — естественно |
Нужно изменить (переключить состояние enum) |
обычно не подходит | |
Свойства — это про «какой ты». Методы — это про «что ты умеешь делать». Если держать это в голове, код получается предсказуемым даже для человека, который видит его впервые (например, для вас через неделю).
8. Типичные ошибки
Ошибка №1: делать computed property с “сюрпризами”.
Новички иногда пишут var message: String { ... }, а внутри меняют глобальные переменные, печатают в консоль, добавляют элементы в массив и вообще ведут себя как метод. Это ломает ожидания: свойство выглядит как «просто чтение», а по факту делает действие. Лучше честно назвать это методом (func makeMessage() -> String или func run(...) -> String) и не вводить читателя в заблуждение.
Ошибка №2: прятать пропущенные cases за default.
Кажется удобным: «да ладно, сделаю default: return "???"». Но вы тем самым отключаете помощь компилятора. Вся сила enum в том, что при добавлении нового case компилятор заставляет пройтись по важным местам. Если вы пишете default, вы говорите: «компилятор, не мешай мне ошибаться». Обычно это плохая сделка.
Ошибка №3: забыть mutating, когда меняете self.
Если внутри метода вы делаете self = .something, Swift справедливо требует mutating. Это не придирка: так язык подчёркивает, что value type меняется. В enum-состояниях (громкость, статус, режим) это встречается постоянно.
Ошибка №4: разносить “таблицу соответствий” по всему проекту.
Классика: в одном месте switch cmd { case .help: ... }, в другом — ещё один switch cmd, и в третьем — третий. Через неделю вы добавляете новый case и забываете обновить один из switch. Если вы уже доросли до идеи «команда — это enum», логично дойти до идеи «команда сама знает, как выполняться» и держать основную логику внутри enum.
Ошибка №5: путать raw value и “человекочитаемый текст”.
Raw value — это чаще всего формат (то, что пользователь вводит, или то, что сохраняется как стабильный идентификатор). А красивый текст — это то, что вы показываете человеку. Иногда они совпадают, иногда нет. Если вы начнёте использовать rawValue как UI-текст, то потом будет больно, когда захотите локализацию или более дружелюбные подписи. Хорошая привычка: raw value оставлять «для машины», а для человека делать отдельное computed property вроде title или usage.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ