JavaRush /Курси /Swift SELF /Вичерпний switch для enum

Вичерпний switch для enum

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

1. Вступ

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

Уявіть, що у вас є статус користувача. Поки варіантів три, ви впевнені, що все обробили. Потім додаєте четвертий, а старий default і далі «прикриває» цей новий варіант… і ви отримуєте поведінку «як вийде». А вдача, як відомо, не входить до стандартної бібліотеки Swift.

2. Що таке вичерпний switch для enum

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

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

Базовий приклад


import Foundation

enum ReadingState {
    case planned
    case reading
    case finished
}

func statusText(for state: ReadingState) -> String {
    switch state {
    case .planned:  return "В планах"
    case .reading:  return "Читаю"
    case .finished: return "Прочитано"
    }
}

Тут важливо не те, що ми повернули рядки (рядки — це просто «видимий результат»), а те, що компілятор змусить нас обробити всі варіанти. Якщо ви додасте case dropped (припинив читати), цей код перестане компілюватися, доки ви не додасте гілку. І це прекрасно: краще нехай компілятор «бурчить» сьогодні, ніж користувач — завтра.

Що буде, якщо додати новий case і забути оновити switch

Зробімо маленький експеримент — подумки або в IDE, як вам зручніше. Додамо новий варіант у enum, але не оновимо switch.

import Foundation

enum ReadingState {
    case planned
    case reading
    case finished
    case dropped
}

func statusText(for state: ReadingState) -> String {
    switch state {
    case .planned:  return "В планах"
    case .reading:  return "Читаю"
    case .finished: return "Прочитано"
    }
}

Такий код не збереться: компілятор скаже, що switch не вичерпний. І це саме те, що нам потрібно: він буквально каже «гай, у тебе є варіант .dropped, а ти його ігноруєш».

Власне, це один із найсильніших «бонусів» enum у Swift. Порівняйте це зі зберіганням стану в рядку: ви додали новий статус "dropped" — і старий код продовжить працювати, просто іноді потраплятиме в else. Тобто помилка стане не помилкою компіляції (найдешевшою), а помилкою під час виконання (найдорожчою і найприкрішою).

3. Коли default шкідливий, а коли потрібний

Із default є тонкий психологічний трюк: він створює відчуття завершеності. Здається, що ви обробили все, адже «є гілка на будь-який випадок». Але ви втрачаєте ключовий захист: компілятор більше не може підказати, що ви забули новий case.

Сценарій із життя навчального проєкту виглядає так:

  1. Ви пишете CLI-команди як enum Command.
  2. Робите switch з default, «щоб не думати».
  3. За тиждень додаєте нову команду.
  4. Застосунок компілюється, але нова команда поводиться як default: наприклад, як «невідома команда», як help або взагалі нічого не робить.
  5. Ви ловите баг не під час компіляції, а тоді, коли користувач пише stats, а застосунок відповідає Unknown command.

Тобто default перетворюється на «килим», під який ви змітаєте сміття. Ніби чисто, але пахне.

Але зауважмо: default буває потрібний. Наприклад, якщо ви перемикаєтеся за Int або String, де варіантів безліч. Або якщо ви працюєте з переліченнями із зовнішніх бібліотек чи SDK, де у майбутніх версіях можуть зʼявитися нові варіанти (у Swift це окрема історія про невичерпні enum у бібліотеках і необхідність гілки на всі інші випадки). Але в межах власних enum на цьому етапі курсу ми вчимося цінувати саме вичерпність без default.

4. Практичні прийоми для читабельного switch

Вичерпний switch корисний не лише для компілятора, а й для вас через тиждень. Він допомагає тримати правила явними: «варіант → поведінка», без прихованої магії.

switch як «таблиця відповідностей»

Коли ви пишете switch за enum, дуже зручно сприймати його як таблицю «варіант → результат». Це особливо корисно для новачків: ви не намагаєтеся тримати в голові складні if/else, ви буквально бачите відповідності.

Наприклад, зробімо рівень доступу в нашому застосунку:

import Foundation

enum AccessLevel {
    case guest
    case member
    case admin
}

func canDeleteBook(_ level: AccessLevel) -> Bool {
    switch level {
    case .guest:  return false
    case .member: return false
    case .admin:  return true
    }
}

Код читається майже як документація: гість — ні, учасник — ні, адміністратор — так. І головне: якщо завтра зʼявиться .moderator, компілятор не дасть забути вирішити, чи може модератор видаляти книжку.

Об’єднання case в одній гілці

Іноді кілька варіантів мають поводитися однаково. У Swift це робиться через кому в одному case. Це допомагає зберігати switch коротким, а отже — простішим для візуальної перевірки.

import Foundation

enum AccessLevel {
    case guest
    case member
    case admin
}

func greeting(for level: AccessLevel) -> String {
    switch level {
    case .guest:
        return "Привіт! Ви можете переглядати каталог."
    case .member, .admin:
        return "Раді знову вас бачити! Каталог і профіль доступні."
    }
}

Тут ми явно показуємо: учасник і адміністратор отримують однакове привітання. І при цьому switch залишається вичерпним: усі варіанти перелічено.

У кожній гілці має бути результат, якщо функція повертає значення

Коли switch стоїть усередині функції, яка повертає значення, вам потрібно, щоб кожна гілка або повертала (return), або якимось чином гарантувала результат. Новачки часто роблять так: у двох гілках return є, а в третій «забули» — і компілятор свариться. Це не прискіпування, а захист від невизначеної поведінки.

Погляньмо на правильний варіант:

import Foundation

enum SortMode {
    case title
    case year
    case author
}

func sortHint(_ mode: SortMode) -> String {
    switch mode {
    case .title:  return "Сортуємо за назвою"
    case .year:   return "Сортуємо за роком"
    case .author: return "Сортуємо за автором"
    }
}

Так, виглядає трохи механічно, зате у функції немає шляху піти без відповіді. Ви буквально закрили всі виходи, крім тих, що вам потрібні.

«Якість» switch: менше магії, більше явних правил

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

Якщо ви звикнете писати вичерпні switch без default для власних enum, то ваш код буде легше розширювати. Компілятор стане вашим «ревʼюером», який завжди на місці й не втомлюється. Він не напише вам красивий коментар у PR, але зате чесно скаже: «додав новий case — онови логіку».

5. Міні-CLI: enum Command і диспетчер команд

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

Почнімо з моделі книжки в спрощеному вигляді. Ми не ускладнюємо поля, бо сьогодні фокус не на struct, а на switch.

import Foundation

struct Book {
    let title: String
}

Тепер оголосімо команди. Тут зручно використовувати raw values (String), щоб команди вводилися текстом. І CaseIterable, щоб можна було красиво показати довідку (ми вже говорили про це раніше).

import Foundation

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

Тепер ключовий шматок: диспетчер команд через вичерпний switch. Гілка на кожен case, без default.

import Foundation

func handle(_ command: Command, library: inout [Book]) -> Bool {
    switch command {
    case .help:
        print("Команди: help, list, add, exit")
        return true
    case .list:
        print("У бібліотеці книжок: \(library.count)")
        return true
    case .add:
        library.append(Book(title: "Нова книжка"))
        print("Книжку додано.")
        return true
    case .exit:
        print("Виходимо...")
        return false
    }
}

Зверніть увагу на деталь, яка здається дрібною, але насправді важлива: функція повертає Bool, щоб керувати циклом — продовжувати роботу чи вийти. І завдяки вичерпному switch компілятор гарантує: для будь-якої команди ми повернемо true або false. Жодних «ой, забули return».

Тепер — цикл читання команд. Ми не заглиблюємося в складніший розбір, а просто використовуємо Command(rawValue:), бо ви вже знаєте, що це Optional і його треба акуратно отримати.

import Foundation

var library: [Book] = []

while true {
    print("> ", terminator: "")
    let input = readLine() ?? ""
    
    guard let cmd = Command(rawValue: input) else {
        print("Невідома команда: \(input)")
        continue
    }
    
    if !handle(cmd, library: &library) { break }
}

Тут вийшла дуже важлива для новачка конструкція «ввід → розбір → switch». І найцінніше ось що: якщо ви за тиждень додасте case stats до enum Command, то компілятор змусить вас оновити handle(...). Жодних «ми забули обробити новий case, але все скомпілювалося».

Щоб зафіксувати це візуально, ось міні-схема потоку:

flowchart TD
    A["readLine()"] --> B{"Command(rawValue: input)?"}
    B -- nil --> C["вивести: невідома команда"] --> A
    B -- cmd --> D{switch cmd}
    D --> E[help]
    D --> F[list]
    D --> G[add]
    D --> H[exit]
    H --> I[break]
    E --> A
    F --> A
    G --> A

6. Типові помилки

Помилка №1: додавати default «щоб компілювалося», а потім забувати про нього.
Такий default часто перетворюється на «звалище логіки»: туди потрапляє все, що ви не захотіли продумати просто зараз. Проблема проявляється пізніше: ви додаєте новий case, очікуєте, що застосунок поводитиметься інакше, а він тихо йде в default і робить не те. Якщо enum ваш власний і закритий, краще явно перелічити всі case і дати компілятору шанс захистити вас.

Помилка №2: не повертати значення з кожної гілки switch, коли switch стоїть у функції, що повертає значення.
Новачок легко пише return у двох гілках, а в третій робить print(...) і думає «ну й гаразд». На щастя, компілятор так не думає: функція зобовʼязана повернути значення на будь-якому шляху виконання. Лікується просто: стежте, щоб кожна гілка або робила return, або виносьте результат у змінну й повертайте його після switch (але на старті це зазвичай менш читабельно).

Помилка №3: намагатися використовувати break «як в інших мовах».
У Swift switch не провалюється до наступної гілки автоматично, тож break наприкінці case зазвичай не потрібний. Через звичку можна почати ставити break усюди, а потім дивуватися, чому в гілці нічого не відбувається (особливо якщо ви випадково написали case .help: break і очікували, що help «якось сам» виведеться).

Помилка №4: змішувати «розбір» і «обробку» в одному величезному switch за рядками.
Коли ви робите switch за inputString і всередині гілок намагаєтеся і перевіряти дані, і виконувати дії, код швидко стає липким, як клавіатура після чаю. Значно чистіше розділяти: спочатку перетворюємо ввід на Command?, а потім робимо switch за Command. Так у вас зʼявляється і типобезпека, і switch стає вичерпним.

Помилка №5: думати, що .caseName можна писати завжди й усюди.
Скорочення .help працює лише тоді, коли тип відомий із контексту (наприклад, змінна вже має тип Command, або параметр функції — Command). Якщо контексту немає, компілятор не зобовʼязаний вгадувати, з якого enum цей .help. У таких місцях пишіть явно Command.help — це не «багатослівність», а ясність.

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