1. Іноді «просто рядок» стає проблемою
Коли ви пишете консольний застосунок, він постійно спілкується із зовнішнім світом. А зовнішній світ спілкується з ним рядками. Користувач вводить "help", "add", "exit". Десь у файлі зберігається "paperback". Десь із API приходить 200 або 404. І спочатку здається: та що тут складного — порівняємо рядок із рядком і підемо далі.
Проблема з’являється не одразу. Вона нагадує про себе тоді, коли ви робите описку ("exiit"), змінюєте регістр ("Help"), додаєте нову команду, забуваєте оновити одне з десяти місць, де порівнювали рядки, і… застосунок починає поводитися дивно. Найприкріше те, що компілятор не може вас захистити, бо для нього "exit" — це просто набір літер.
Raw values потрібні саме тут: ми хочемо, щоб усередині програми усе було типобезпечно (enum Command), а зовні — стабільно й передбачувано ("exit" або 0), і щоб між цими світами було безпечне перетворення.
2. Raw values в enum
Raw value — це значення типу String або Int, яке жорстко пов’язане з конкретним case. Тобто кожен варіант отримує свій «паспорт»: саме так його бачить зовнішній світ.
Синтаксис має такий вигляд:
import Foundation
enum Command: String {
case help
case exit
}
Тут Command — це як і раніше enum, але тепер він «підкріплений» рядком. Під капотом Swift робить так, що в кожного case є rawValue.
Важливо: у межах цього уроку ми працюємо тільки з enum без «додаткових даних» усередині кейса. Тобто case help, але не case add(title: String) — такі речі будуть пізніше.
rawValue: як отримати зовнішнє представлення
Коли в enum є raw values, у кожного значення з’являється властивість rawValue.
Давайте почнемо з максимально простого прикладу:
import Foundation
enum Command: String {
case help
case exit
}
let cmd: Command = .help
print(cmd.rawValue) // help
Зверніть увагу на тонкість: ми друкуємо не cmd (це Command), а саме cmd.rawValue (це String). Тобто rawValue — це те, як команда виглядає в текстовому вигляді.
Це зручно, коли ви хочете, наприклад, надрукувати підказку користувачу у форматі, який він має вводити:
import Foundation
enum Command: String {
case help
case exit
}
print("Доступні команди: \(Command.help.rawValue), \(Command.exit.rawValue)")
// Доступні команди: help, exit
У цей момент enum починає працювати як «єдине джерело правди». Команда називається .help, а назовні вона завжди виглядає як "help".
Автоматичні та явні raw values
Коли raw value — String, Swift уміє автоматично підставляти рядок за імʼям case. Тобто case help → "help". Це виглядає зручно… доки ви не згадаєте, що назви кейсів іноді хочеться перейменувати заради читабельності, а формат введення чи файлу має залишатися стабільним.
З Int інша історія: Swift уміє автоматично нумерувати кейси, починаючи з 0, якщо ви не вказали конкретних чисел.
Щоб не тримати це в голові, зафіксуємо в невеликій таблиці:
| Основа raw value | Якщо значення не задано явно | Приклад |
|---|---|---|
|
рядок збігається з іменем case | |
|
автонумерація (зазвичай із 0) | |
Тут і народжується практичне правило: якщо raw value є частиною формату, який вводить користувач (CLI-команди) або який зберігається у файлі, краще замислитися над явними значеннями. Бо перейменування кейса — нормальний рефакторинг, а поломка формату — не дуже.
Приклад із явними рядковими raw values:
import Foundation
enum Command: String {
case help = "help"
case exit = "exit"
}
На перший погляд це схоже на «масло масляне», але в цього підходу є плюс: ви явно показуєте, що "help" — це формат, а не просто побічний ефект від імені case.
Зворотне перетворення: init?(rawValue:) і Optional
Ми навчилися перетворювати enum на рядок через rawValue. Тепер головне: як перетворити рядок назад на enum.
Для цього в enum з raw values автоматично з’являється ініціалізатор:
Command(rawValue: "help") // Command?
Ключовий момент: результат — Optional. Це не «примха Swift», а захист: рядок "helpp" не повинен магічно перетворюватися на команду. Він має чесно стати nil.
Такий ініціалізатор називається failable initializer: він може не створити значення і повернути nil. У Swift сама ідея «ініціалізатор може зазнати невдачі й повернути nil» — цілком нормальна частина мови: такі ініціалізатори справді мають право завершитися return nil.
Приклад:
import Foundation
enum Command: String {
case help
case exit
}
let input = "exit"
if let cmd = Command(rawValue: input) {
print("Команду розпізнано: \(cmd.rawValue)") // Команду розпізнано: exit
} else {
print("Невідома команда: \(input)")
}
Тут ми робимо саме те, чого навчилися раніше: якщо значення є — працюємо з ним, якщо немає — повідомляємо про помилку.
Іноді хочеться скоротити код — можна використати ??, але тут потрібно бути уважними, бо він може «з’їсти» помилку:
import Foundation
enum Command: String {
case help
case exit
}
let input = "unknown"
let cmd = Command(rawValue: input) ?? .help
print(cmd.rawValue) // help
Це допустимо, коли ви справді хочете: «не розпізнали — вважаємо, що help». Але якщо ви пишете реальний CLI, частіше краще явно повідомити, що команда невідома.
Мінісхема: конвеєр парсингу команди
Перш ніж писати код для навчального CLI, корисно побачити процес цілком — як короткий конвеєр. Це той випадок, коли блок-схема економить кілька хвилин на з’ясування, чому воно nil.
flowchart TD
A["readLine() -> String?"] --> B["розпакувати Optional (guard / if let)"]
B --> C["обрізати пробіли / lowercasing"]
C --> D["взяти перший токен"]
D --> E["Command(rawValue: token) -> Command?"]
E -->|some| F["виконати команду"]
E -->|nil| G["повідомити: невідома команда"]
Ми не робимо нічого магічного: просто акуратно приводимо ввід до потрібного формату й намагаємося створити enum.
3. Приклад: LibraryCLI та розпізнавання команди
Уявімо, що в нас уже є простенький цикл читання команд (його ми вже бачили, коли вивчали while і введення). Зараз наша мета скромна: розпізнати лише дієслово команди. Аргументи (наприклад, назву книги) поки залишимо рядками, бо моделювання «команда + аргументи» — це вже наступний крок і окрема тема.
Почнемо з enum команд:
import Foundation
enum LibraryCommand: String {
case help
case list
case exit
}
Тепер напишемо маленьку функцію parseCommand(_:), яка приймає рядок і намагається витягти команду з першого токена:
import Foundation
func parseCommand(_ line: String) -> LibraryCommand? {
let cleaned = line.trimmingCharacters(in: .whitespacesAndNewlines)
let parts = cleaned.split(separator: " ")
guard let first = parts.first else { return nil }
return LibraryCommand(rawValue: String(first))
}
Зверніть увагу, що ми діємо обережно:
Ми очищаємо пробіли по краях, ріжемо рядок на частини, беремо першу частину й перетворюємо Substring на String, бо rawValue у нас рядковий.
Тепер можна використовувати:
import Foundation
let line = "list books"
let cmd = parseCommand(line)
print(cmd?.rawValue ?? "nil") // list
Так, поки що це мінімалізм, але вже корисний: ми перестали порівнювати рядки "list" == ... і отримали тип LibraryCommand.
Трохи кращий UX: робимо команди нечутливими до регістру
Користувач обов’язково введе "HELP", бо Caps Lock — це окрема форма життя. Підтримаємо це простим lowercased():
import Foundation
func parseCommand(_ line: String) -> LibraryCommand? {
let cleaned = line.trimmingCharacters(in: .whitespacesAndNewlines)
let parts = cleaned.split(separator: " ")
guard let first = parts.first else { return nil }
let token = String(first).lowercased()
return LibraryCommand(rawValue: token)
}
Тепер "HELP" стане "help", і команду буде розпізнано.
4. Int raw values: меню та числові коди
Рядкові raw values добре підходять для команд, які користувач вводить словами. Але інколи інтерфейс побудований інакше: ви друкуєте меню й просите ввести число. Вводити "1" багатьом простіше, ніж "list".
Ось тут Int-raw values стають дуже практичними.
Приклад меню:
import Foundation
enum MenuChoice: Int {
case list = 1
case help = 2
case exit = 0
}
Сенс читається майже без коментарів: 0 — вийти, 1 — показати список, 2 — допомога.
Тепер парсинг має такий вигляд:
import Foundation
let inputNumber = 2
if let choice = MenuChoice(rawValue: inputNumber) {
print("Ви вибрали: \(choice)") // Ви вибрали: help
} else {
print("Невідомий пункт: \(inputNumber)")
}
У реальному введенні ви отримаєте рядок, потім перетворите його на Int, а потім — на MenuChoice?:
import Foundation
let line = "1"
let number = Int(line) ?? -1
let choice = MenuChoice(rawValue: number)
print(choice?.rawValue ?? -1) // 1
Тут видно два місця, де можна помилитися: перетворення String -> Int і перетворення Int -> enum. І обидва кроки чесно дають нам змогу обробити помилку.
5. Raw value — це формат, а не «красивий текст»
Є тонка плутанина, у яку потрапляють майже всі: raw value починають використовувати як «людинозрозуміле ім’я». Іноді це збігається, але не завжди.
Уявіть, що в вас є жанри книжок. Для файлу ви хочете стабільні коди на кшталт "sci-fi", "non-fiction", а користувачу показувати «Наукова фантастика» і «Нон-фікшн». Це різні задачі, і змішувати їх незручно.
Зробімо так: raw values — для формату, а красивий текст — через computed property. (Computed properties у enum нам уже траплялися: тип може містити й поведінку.)
import Foundation
enum Genre: String {
case sciFi = "sci-fi"
case nonFiction = "non-fiction"
var title: String {
switch self {
case .sciFi: return "Наукова фантастика"
case .nonFiction: return "Нон-фікшн"
}
}
}
Тепер у нас є стабільний формат:
import Foundation
let g: Genre = .sciFi
print(g.rawValue) // sci-fi
print(g.title) // Наукова фантастика
І це позбавляє проблем, коли ви раптово захочете перейменувати .sciFi на .scienceFiction заради читабельності коду: raw value залишиться "sci-fi", а файл і користувацький ввід не зламаються.
6. Прийом self.init(rawValue:) у власних ініціалізаторах
Іноді ви хочете зробити ініціалізатор не безпосередньо init?(rawValue:), а, наприклад, «із рядка, але з очищенням». Усередині такого ініціалізатора часто використовують прийом: спочатку підготувати значення, а потім викликати self.init(rawValue: prepared).
У документації та прикладах стандартного підходу до кодування можна побачити саме цей прийом: спочатку підготувати rawValue, а потім делегувати в self.init(rawValue: rawValue).
Ми можемо зробити подібне в навчальних цілях, наприклад для жанру:
import Foundation
enum Genre: String {
case sciFi = "sci-fi"
case nonFiction = "non-fiction"
init?(userInput: String) {
let cleaned = userInput.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = cleaned.lowercased()
self.init(rawValue: normalized)
}
}
Використання:
import Foundation
let input = " SCI-FI "
let genre = Genre(userInput: input)
print(genre?.rawValue ?? "nil") // sci-fi
Сенс простий: ми додаємо «розумне» введення для людини, але фінальне рішення все одно ухвалює init?(rawValue:).
7. Типові помилки
Помилка №1: очікувати, що Enum(rawValue:) завжди повертає значення, і ігнорувати Optional.
Це класика: студент пише let cmd: Command = Command(rawValue: input) і дивується, чому компілятор свариться. Компілятор має рацію: ввід може бути будь-яким, отже результат — Command?. Лікується це не спробою впхнути !, а нормальною розпаковкою через if let/guard let або осмисленим дефолтом ?? (якщо дефолт справді доречний).
Помилка №2: використовувати ! на зовнішньому вводі.
Command(rawValue: input)! виглядає як короткий шлях до щастя, але на практиці це короткий шлях до падіння застосунку через будь-яку описку користувача. Зовнішній ввід за визначенням ненадійний, і init?(rawValue:) спеціально зроблений як failable initializer, тобто ініціалізатор, який може законно завершитися nil.
Помилка №3: плутати ім’я case і raw value, особливо з String.
Поки raw value генерується автоматично, здається, що це одне й те саме. Але варто вам задати case sciFi = "sci-fi", і раптом стає очевидно: ім’я кейса — для коду, raw value — для формату. Якщо ви порівнюєте ввід користувача з "sciFi" замість "sci-fi", команда чи жанр не розпізнаються, і ви довго дивитеся на екран із виразом «але я ж майже вгадав».
Помилка №4: «ховати» помилку розпізнавання під ?? там, де потрібен явний unknown.
Іноді роблять let cmd = Command(rawValue: input) ?? .help і думають, що зробили UX кращим. Насправді це часто погіршує поведінку: користувач увів "lisst", а програма мовчки показує help, ніби користувач сам попросив допомоги. Якщо ви робите CLI, зазвичай коректніше чесно сказати «unknown command» і показати підказку окремо.
Помилка №5: робити raw value «красивим текстом» і потім мучитися під час локалізації та редагування.
Якщо ви зберігаєте raw value як «Наукова фантастика», ви прив’язуєте формат даних до мови інтерфейсу і до пунктуації. Сьогодні ви додасте крапку, завтра зміните регістр, і раптом старі дані не завантажаться. Набагато надійніше тримати raw value як стабільний код ("sci-fi"), а красивий текст повертати через computed property на кшталт title.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ