JavaRush /Курси /Swift SELF /Raw values та init?(rawValue:) у Swift

Raw values та init?(rawValue:) у Swift

Swift SELF
Рівень 25, Лекція 1
Відкрита

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 Якщо значення не задано явно Приклад
String
рядок збігається з іменем case
case inProgress → "inProgress"
Int
автонумерація (зазвичай із 0)
case small, medium, large → 0, 1, 2

Тут і народжується практичне правило: якщо 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.

1
Задача
Swift SELF, 25 рівень, 1 лекція
Недоступна
Коди доставки
Коди доставки
1
Задача
Swift SELF, 25 рівень, 1 лекція
Недоступна
Статус платежу
Статус платежу
1
Задача
Swift SELF, 25 рівень, 1 лекція
Недоступна
Меню застосунку
Меню застосунку
1
Задача
Swift SELF, 25 рівень, 1 лекція
Недоступна
Перша команда
Перша команда
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ