JavaRush /Курси /Swift SELF /CaseIterable — список значень

CaseIterable — список значень

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

1. CaseIterable і allCases

Коли ви пишете невеликий консольний застосунок, спокуса дуже велика: «ну список команд невеликий, зараз просто надрукую його як текст». На старті це здається безпечним, але в такого підходу є неприємна властивість: він ламається не відразу, а після змін.

Уявіть: ви додали нову команду .stats, але забули оновити текст help. Застосунок компілюється, запускається, та користувачі не дізнаються про нову команду. Або ще гірше: ви видалили команду, а help усе ще її показує. Це не «фатальна помилка», але вже технічний борг — маленький, липкий і неприємний.

Ідеальна мета — щоб список команд для help автоматично отримувався з enum, а будь-яке додавання або видалення case одразу відображалося в довідці. Саме це й робить CaseIterable.

Що таке CaseIterable і як його підключити

CaseIterable — це протокол стандартної бібліотеки Swift, який означає: «у цього типу є скінченний набір значень, і його можна перелічити». Для enum без додаткових даних компілятор уміє сам генерувати реалізацію — але лише якщо ви явно про це попросите.

За контрактом це виглядає так: у типу зʼявляється статична властивість allCases, яка повертає колекцію всіх варіантів. Формально протокол улаштований приблизно так: associatedtype AllCases: Collection і static var allCases: AllCases { get }.

Важливо: автоматична генерація працює для простих enum, тобто таких, де варіанти не зберігають додаткових значень (ми поки що працюємо саме з такими). Також компілятор синтезує allCases лише тоді, коли відповідність протоколу вказано прямо в оголошенні enum.

Давайте почнемо з найкоротшого прикладу.


import Foundation

enum Command: CaseIterable {
    case help, exit
}

print(Command.allCases.count) // 2

Тут уже видно нову ідею: список варіантів — це властивість типу, а не конкретного значення.

allCases — властивість типу, а не екземпляра

У Swift і взагалі в мовах зі статичними членами діє проста логіка: якщо річ стосується всіх можливих значень типу, їй місце на типі. allCases — саме така річ: це «всі варіанти команди взагалі», а не «варіанти конкретної команди .help».

Тому ми пишемо Command.allCases, а не cmd.allCases.

import Foundation

enum Mood: CaseIterable {
    case happy, neutral, sad
}

let m: Mood = .happy
print(Mood.allCases.count) // 3
// print(m.allCases)       // ❌ так не можна: allCases — не властивість екземпляра

Якщо ви зловите себе на думці «ех, шкода, що не можна m.allCases», не хвилюйтеся: це як запитати в яблука список усіх фруктів. Яблуко, звісно, старається, але лапок у нього немає.

allCases — колекція, а не обовʼязково Array

Коли ви бачите allCases, дуже хочеться подумати: «це масив». У побуті так і можна думати, але з важливим застереженням: за контрактом allCases повертає якусь колекцію, а не обовʼязково масив Enum.

Практичний висновок простий: ви можете робити те, що вміє колекція, — наприклад, дізнаватися count і проходитися for-in.

import Foundation

enum Command: CaseIterable {
    case help, exit
}

for cmd in Command.allCases {
    print(cmd)
}
// help
// exit

Якщо вам раптом дуже потрібне індексування, як у масиву, наприклад «команда під номером 2», тоді ви можете явно перетворити allCases на масив.

import Foundation

enum Mood: CaseIterable {
    case happy, neutral, sad
}

let list = Array(Mood.allCases)
print(list[0]) // happy

Щоб закріпити, ось маленька таблиця «що можна робити» з allCases:

Що робимо Приклад Навіщо це в CLI
Дізнатися кількість
Command.allCases.count
«Скільки команд підтримуємо?»
Перебрати
for c in Command.allCases { ... }
Виведення довідки
Отримати нумерацію
Command.allCases.enumerated()
Зручна довідка зі списком 1..N
Індексувати
Array(Command.allCases)[i]
Меню вибору за номером, якщо раптом знадобиться

2. Приклад: команди CLI

Від цього місця зберемо один мініпроєкт: консольний застосунок «бібліотека» — поки що дуже простий. Ми ще не будуємо ідеальну архітектуру та не зберігаємо книги у файлах. Наша мета сьогодні скромніша: зробити список команд і довідку, яка оновлюється автоматично.

Спочатку оголосимо список команд. Ми хочемо, щоб команди були стабільно представлені рядками, бо користувач вводить текст. Отже: RawRepresentable через String і CaseIterable для списку.

import Foundation

enum LibraryCommand: String, CaseIterable {
    case help
    case add
    case list
    case remove
    case exit
}

Тут важливо: rawValue автоматично дорівнює імені цього caseʼа (наприклад, "add"), тому що ми використовуємо String-raw-values і не задаємо їх вручну.

3. Help/довідка з allCases

Просте виведення help

Тепер зробимо функцію, яка друкує список команд. Зверніть увагу: ми не пишемо вручну "help, add, list...". Замість цього беремо LibraryCommand.allCases і виводимо кожну команду.

import Foundation

func printHelp() {
    print("Доступні команди:")
    for cmd in LibraryCommand.allCases {
        print("- \(cmd.rawValue)")
    }
}

printHelp()

Вивід буде приблизно такий:

Доступні команди:
- help
- add
- list
- remove
- exit

З погляду підтримки коду це вже перемога: додали новий case — help оновився автоматично. Видалили case — help теж оновився.

Нумерована довідка

У CLI люди люблять списки «1) 2) 3)», бо так їх легше читати. Для цього використаємо enumerated().

import Foundation

func printHelp() {
    print("Доступні команди:")
    for (i, cmd) in LibraryCommand.allCases.enumerated() {
        print("\(i + 1). \(cmd.rawValue)")
    }
}

printHelp()

Тут i починається з 0, тому ми друкуємо i + 1. Це класичний момент «індекси з нуля» — і одна з найдавніших причин, чому програмісти іноді виглядають втомленими.

Мінісхема: enum → allCases → help

Щоб закріпити звʼязок «enum → allCases → help», корисно уявити це як маленький конвеєр:

flowchart LR
    A["enum LibraryCommand"] --> B["LibraryCommand.allCases"]
    B --> C["for-in / enumerated()"]
    C --> D["виведення довідки/help"]

Сенс цієї схеми в тому, що help — не окремий «шматок тексту», а похідна від типу команд.

Чому CaseIterable особливо корисний для довідки

У CLI команда help — це не прикраса. Це «вихід із лабіринту», коли користувач або ви самі через тиждень забули синтаксис. І CaseIterable дозволяє реалізувати help так, щоб він був: узгодженим із реальністю, стійким до змін і з мінімумом дублювання коду. Ми не тримаємо окремий список команд вручну.

Якщо коротко: CaseIterable допомагає тримати одне джерело правди. Джерело правди — це enum LibraryCommand. А help — це просто «подання» цього enum для користувача.

Саме тому в Swift CaseIterable улаштований як opt-in протокол із allCases: ви явно кажете, що тип скінченний і його можна перелічувати, а потім користуєтеся цим як звичайною колекцією.

4. Розбір введення в інтерактивному циклі

Тепер зберемо мініінтерактив: застосунок запитує команду, читає рядок, намагається розпізнати його як LibraryCommand, а якщо не вдається — друкує підказку і help.

Зверніть увагу: ми використовуємо init?(rawValue:), який повертає Optional. Це чесна модель зовнішнього світу: користувач може ввести будь-що, а не лише «правильні» команди.

Читання рядка і спроба розпізнати команду

import Foundation

while true {
    print("Введіть команду (help для довідки):", terminator: " ")
    let input = readLine() ?? ""
    
    if let cmd = LibraryCommand(rawValue: input) {
        print("Гаразд, команда: \(cmd.rawValue)")
    } else {
        print("Невідома команда: \(input)")
        printHelp()
    }
}

Так, цикл нескінченний — ми ще не навчили його зупинятися. Виправимо це прямо зараз, але обережно: зробимо мінімальну перевірку.

Вихід через .exit і обробка .help

import Foundation

while true {
    print("Введіть команду (help для довідки):", terminator: " ")
    let input = readLine() ?? ""
    
    guard let cmd = LibraryCommand(rawValue: input) else {
        print("Невідома команда: \(input)")
        printHelp()
        continue
    }
    
    if cmd == .exit {
        print("Вихід із програми.")
        break
    }
    
    if cmd == .help {
        printHelp()
    } else {
        print("Команду прийнято: \(cmd.rawValue)")
    }
}

Тепер у нас уже робочий CLI: він уміє показувати довідку, скаржитися на невідоме і виходити.

Нормалізація введення: lowercased()

Користувач може ввести HELP, Help, HeLp і очікувати, що ви його зрозумієте. Якщо формат команд жорстко задано як lowercase, можна зробити крок назустріч: приводити введення до нижнього регістру перед парсингом.

Це не обовʼязкова частина CaseIterable, але для CLI це дуже практичний крок. І він добре поєднується з raw-значеннями: raw-значення залишаються стабільними, а ми просто робимо введення більш терпимим.

import Foundation

let input = (readLine() ?? "").lowercased()

if let cmd = LibraryCommand(rawValue: input) {
    print("Команда: \(cmd.rawValue)")
} else {
    print("Невідома команда: \(input)")
}

Якщо ви так зробите, help і далі виводитиме команди в нижньому регістрі, що збігається з канонічною формою.

5. Типові помилки під час роботи з CaseIterable і allCases

Помилка №1: очікувати, що allCases зʼявиться «сам по собі».
Новачки часто думають: «Раз enum скінченний, значить у нього точно є список варіантів». Але в Swift потрібно явно вказати : CaseIterable. Без цього allCases не буде, бо участь у механізмі перелічення — усвідомлене рішення розробника, і компілятор не буде «магічно» робити це за вас без запиту.

Помилка №2: звертатися до allCases в екземпляра, а не в типу.
Плутають cmd.allCases і LibraryCommand.allCases. Це логічно неправильно: екземпляр — це одне значення, а allCases — це «всі можливі значення типу». Якщо ловите цю помилку, зупиніться і подумки замініть cmd на «яблуко», а LibraryCommand — на «фрукт».

Помилка №3: думати, що allCases — це обовʼязково [LibraryCommand].
За контрактом це не масив, а «якась колекція». Іноді все одно зручно робити Array(LibraryCommand.allCases), але краще не будувати код так, ніби allCases завжди індексується як масив. Перебір for-in майже завжди простіший і надійніший.

Помилка №4: намагатися використовувати CaseIterable для enum із даними всередині case.
Сьогодні ми працюємо лише з простими enum. Якщо в case зберігаються додаткові значення, автоматична генерація allCases не спрацює, бо «всі варіанти» стають нескінченними (наприклад, .setVolume(Int) — це скільки варіантів? Нескінченно). Тому в межах цієї теми тримайтеся простих case без параметрів.

Помилка №5: тримати список команд окремо й забувати його оновлювати.
Іноді роблять так: let commands = ["help", "add", "list"], а enum існує окремо. Це знову повертає вас у світ «двох джерел правди»: одне в enum, друге — в масиві рядків. Через кілька змін вони обовʼязково розʼїдуться. CaseIterable якраз і потрібний, щоб цього не ставалося.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ