1. Команды и аргументы в CLI
Когда мы пишем CLI-приложение (консольную утилиту), мы почти всегда мыслим в терминах команд: «добавь книгу», «покажи список», «удали по индексу», «найди по слову». Проблема в том, что команда редко бывает просто словом. Обычно ей нужны данные: какую книгу добавить, какой индекс удалить, что искать. И вот тут обычный enum «без начинки» начинает явно голодать.
Представьте, что команда — это заказ в кофейне. Сказать «кофе» — недостаточно (и бариста посмотрит на вас как на fatalError() в пятницу вечером). Нужно: «капучино, 300 мл, без сахара». Так вот, associated values — это и есть способ сказать: «вариант один, но с конкретными данными».
Ограничение простого enum
Простой enum хорош, когда вам нужно выбрать один вариант, и больше ничего. Например: направление сортировки (ascending/descending), тема интерфейса (light/dark), состояние «вкл/выкл» и так далее. Он похож на переключатель: щёлк — и у вас одно из заранее известных положений.
Но как только у каждого варианта появляется «полезная нагрузка», простой enum превращается в чемодан без ручки. Он может сказать «удали», но не может сказать «удали вот это конкретное». Он может сказать «добавь», но не может сказать «добавь книгу с таким-то названием». И если попытаться «доприкрутить» это отдельными переменными, то быстро начинается хаос: команда одна, а данные рядом лежат «на честном слове», и легко рассинхронизируются.
Associated values простыми словами
Associated values (связанные значения) — это данные, которые хранятся внутри конкретного кейса enum. То есть enum по-прежнему гарантирует «ровно один вариант», но теперь у некоторых вариантов есть «встроенные аргументы».
Важно почувствовать именно модель: enum — это “один из вариантов”, и у каждого варианта может быть свой набор данных. Это ключевое отличие от struct, где «все поля есть всегда».
Ниже — самый минимальный пример. Здесь команда «переименовать» не может существовать без нового имени, поэтому имя — часть кейса.
import Foundation
enum Command {
case help
case rename(newTitle: String)
}
let c1: Command = .help
let c2: Command = .rename(newTitle: "Чистый код")
print(c1)
print(c2)
Обратите внимание на ощущение: .rename(newTitle: "...") выглядит почти как вызов функции. И это не случайность: у кейса с associated values действительно есть «конструктор», и его вызов очень похож на вызов функции.
2. Команды для LibraryCLI
«Команда + аргументы» как удобная модель
Теперь приземлим это на нашу учебную реальность. Мы постепенно двигаемся к CLI-приложению (условно назовём его LibraryCLI), которое управляет библиотекой книг. На этом шаге нам важно не «идеальное хранение книг», а именно модель команд, чтобы в коде было сложно сделать бессмысленное действие.
Для начала создадим простую модель книги. Вы уже умеете struct, поэтому пусть будет так (минимально, без усложнений).
import Foundation
struct Book {
let title: String
let author: String
}
let book = Book(title: "Dune", author: "Frank Herbert")
print(book.title) // Dune
А теперь — самое интересное: команды. Какие варианты нам нужны? Например: добавить книгу, удалить книгу по индексу, показать список, выйти.
import Foundation
enum Command {
case add(title: String, author: String)
case remove(index: Int)
case list
case quit
}
let cmd: Command = .add(title: "Dune", author: "Frank Herbert")
print(cmd)
И вот здесь мы делаем первый важный вывод: разные кейсы могут требовать разные данные. У .list данных нет, у .remove есть индекс, у .add есть два поля. Это ровно тот случай, когда enum — идеальный инструмент.
Почему лейблы — это страховка, а не украшение
Когда associated values больше одного, появляется риск «перепутать местами». Если вы храните (String, String) без лейблов, то через неделю сами же не вспомните: первое — автор или название? А компилятор тем более не телепат.
Swift позволяет задавать лейблы прямо в объявлении кейса, и это резко повышает читабельность и снижает число ошибок. Более того, эти лейблы участвуют в «полном имени» конструктора кейса, почти как в функциях: они становятся частью того, как вы вызываете этот кейс.
Сравним два подхода — «плохой» (без лейблов) и «хороший» (с лейблами).
Пример (без лейблов, так лучше не делать):
import Foundation
enum Command {
case add(String, String)
}
let cmd: Command = .add("Frank Herbert", "Dune") // Ой... а это точно правильно?
print(cmd)
Пример (с лейблами, так лучше делать):
import Foundation
enum Command {
case add(title: String, author: String)
}
let cmd: Command = .add(title: "Dune", author: "Frank Herbert")
print(cmd)
Во втором варианте код читается как предложение: «add title: Dune author: Frank Herbert». И это не просто красиво — это снижает шанс, что вы случайно «переедете» смысл аргументов бульдозером.
Небольшая сборка: история команд
Пусть пока без парсинга строк. Представим, что команда «родилась» в коде (например, в тестовом сценарии или в отладочной ветке). Сделаем массив истории — просто чтобы почувствовать, что Command это значение, и оно переносимо.
import Foundation
enum Command {
case add(title: String, author: String)
case list
case remove(index: Int)
}
let history: [Command] = [
.add(title: "Dune", author: "Frank Herbert"),
.add(title: "1984", author: "George Orwell"),
.list,
.remove(index: 0)
]
print(history.count) // 4
Это уже выглядит как «история действий», которую можно анализировать, логировать или повторять. И всё это — без классов, без наследования и без магии: просто типы и значения.
3. Дизайн enum: как не сделать кашу
struct vs enum: как выбрать
Когда люди впервые видят enum с associated values, возникает соблазн: «О! Значит можно вообще всё делать enum-ами?» Можно, но не нужно. Как и с кофе: можно пить 8 эспрессо в день, но потом ваш sleep() будет возвращать nil.
Выбор обычно такой: если у сущности всегда есть одни и те же поля — это struct. Если у сущности есть ровно один вариант из нескольких, и поля зависят от выбранного варианта — это enum.
Для команд CLI это почти всегда enum, потому что команда — это вариант действия, и набор аргументов зависит от варианта.
Небольшая таблица для закрепления:
| Вопрос | Если ответ «да» — чаще подходит |
|---|---|
| «У объекта всегда есть все поля?» | |
| «Объект всегда ровно одного типа, но с разными вариантами?» | |
| «Набор данных зависит от выбранного варианта?» | с associated values |
| «Я хочу запретить невозможные комбинации данных на уровне типов?» | с associated values |
Типичная анти-модель новичка — сделать “универсальную команду” со множеством Optional полей. Это выглядит так: struct Command { var title: String?; var index: Int?; var mode: String }. А потом начинается цирк: команда “list”, но почему-то index тоже заполнен, а title пустой… и никто не знает, что является ошибкой, а что «ну так получилось».
enum с associated values эту проблему решает в корне: невозможные состояния нельзя создать (или их очень сложно создать, если вы не стараетесь специально).
Практический дизайн кейсов
Когда вы проектируете enum команд, главный вопрос — где провести границу между «много маленьких кейсов» и «один кейс, но с кучей данных». Хорошая новость: Swift даёт вам инструменты сделать модель читаемой. Плохая новость: иногда мы сами себе враги.
Полезное правило: кейс должен отражать намерение. Если намерение меняется — это другой кейс. Если намерение одно и то же, но параметры отличаются — это один кейс с associated values.
Например, .remove(index: Int) и .removeAll — это два разных намерения. Значит, два кейса:
import Foundation
enum Command {
case remove(index: Int)
case removeAll
}
А вот «добавить книгу» всегда одно намерение, просто параметры (title/author) являются частью этого намерения. Значит, один кейс с данными.
Ещё полезный момент: если вы видите, что у кейса 5–6 параметров, стоит на секунду остановиться и спросить себя: «А не пытаюсь ли я упаковать в одну команду несколько разных?» Иногда ответ «да», и модель стоит разделить.
Кейсы с associated values как конструкторы
Этот раздел полезен, чтобы лучше понимать сообщения компилятора и автодополнение в IDE. В Swift кейс enum с associated values можно воспринимать как функцию-конструктор: он «принимает параметры и возвращает значение enum». Это почему вызов .add(title:author:) выглядит как вызов функции.
Более того, лейблы являются частью «полного имени» такого конструктора, и это сделано специально, чтобы уменьшать неоднозначность и повышать читаемость.
Для нас практический вывод простой: не стесняйтесь лейблов. Они не «для красоты», они для того, чтобы вы меньше ошибались.
4. Извлечение данных и типичные сценарии
Мини-схема: как команда живёт в программе
Чтобы сложилось цельное ощущение, полезно представить путь команды внутри программы. Сейчас мы ещё не парсим ввод «по-настоящему» (это будет отдельная тема), но уже можем мыслить так: ввод → команда как значение → выполнение.
flowchart TD
A[Пользователь вводит строку] --> B[Мы получаем данные]
B --> C["Создаём Command (enum)"]
C --> D[Передаём Command в логику]
D --> E[Команда выполнена / выведено сообщение]
Ключевой момент: Command — это обычное значение, как Int или String. Его можно хранить в переменной, передавать в функцию, класть в массив истории команд (если захотим), логировать и так далее. И именно associated values позволяют этому значению быть «самодостаточным».
Associated values — не свойства
Здесь студенты часто спотыкаются об ожидание из мира struct: «Раз команда .add(...), значит я могу сделать command.title». Нельзя. У enum нет гарантии, что поле title существует всегда: оно есть только у .add, а у .list его нет вообще, и это логично.
То есть associated values не читаются как обычные свойства. Чтобы получить данные, нужно «распаковать» кейс через сопоставление с шаблоном (pattern matching). Мы пока не углубляемся в синтаксис распаковки, но важно запомнить сам принцип: данные внутри enum достаются не точкой, а сопоставлением.
Чтобы всё-таки показать идею без деталей, вот пример, где мы проверяем только «какой кейс», не доставая данные.
import Foundation
enum Command {
case add(title: String, author: String)
case list
}
func isList(_ cmd: Command) -> Bool {
switch cmd {
case .list:
return true
case .add:
return false
}
}
Сейчас нам важна мысль: даже если данные пока не достали, enum уже помогает держать код в форме — вариантов ограниченное число, и компилятор следит, чтобы мы их не «забыли».
Ещё один пример: состояния с данными
Associated values — это не только про команды. Очень частый кейс — моделирование состояний процесса: «ничего не происходит», «загружаем», «успех», «ошибка». И у некоторых состояний есть полезные данные: прогресс, массив результатов, сообщение об ошибке.
В консольном приложении библиотеки мы тоже можем иметь состояния: «пусто», «есть книги», «ошибка при чтении». (Да, сегодня мы не читаем файлы, но состояние как идея — уже полезно.)
import Foundation
enum LibraryState {
case empty
case hasBooks(count: Int)
case failed(message: String)
}
let state: LibraryState = .hasBooks(count: 3)
print(state)
Это намного выразительнее, чем набор разрозненных переменных вроде var count = 3; var error: String? = nil; var isEmpty = false, которые могут противоречить друг другу.
5. Типичные ошибки
Ошибка №1: путать raw values и associated values.
Raw value — это одно значение, которое соответствует кейсу как «внешний ярлык» (например, строка "add" или число 1). Associated values — это данные, которые живут внутри конкретного случая и могут быть разными по типам и количеству. Если вы начинаете пытаться «засунуть автора книги в raw value», остановитесь и выдохните: raw values не для этого.
Ошибка №2: делать кейсы без лейблов, а потом путаться в порядке аргументов.
Поначалу кажется, что .add("Dune", "Frank Herbert") быстрее печатать. Но через пару дней вы (или ваш напарник) начнёте гадать, где название, а где автор. С лейблами код становится самодокументируемым, и это окупается быстрее, чем кажется. К тому же лейблы участвуют в имени конструктора кейса, что помогает избегать неоднозначностей.
Ошибка №3: пытаться обращаться к associated values через точку (command.title).
Такое желание — нормальный «рефлекс после struct». Но у enum нет гарантии, что поле существует во всех кейсах. Поэтому и нельзя. Правильная мысль: «сначала выясняю, какой кейс, потом извлекаю данные». Сегодня достаточно просто запомнить принцип, не пытаясь обходить его хитростями.
Ошибка №4: делать “универсальный” тип команды с кучей Optional полей вместо нормального enum.
Эта ошибка коварна тем, что сначала всё компилируется и даже «как будто работает». А потом у вас появляется команда "list", у которой почему-то заполнен title, и начинается расследование уровня детектива: «кто положил это сюда и зачем». enum с associated values как раз и нужен, чтобы подобных невозможных комбинаций не существовало.
Ошибка №5: раздувать один кейс до состояния «комбайна».
Если вы делаете case edit(title: String?, author: String?, year: Int?, remove: Bool, force: Bool) — это сигнал, что вы смешали несколько разных намерений. Такие модели трудно читать и трудно поддерживать: получается не команда, а швейцарский нож с торчащими лезвиями. Лучше разделить на несколько кейсов, каждый из которых будет выражать одно действие понятно и прямо.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ