JavaRush /Курси /Swift SELF /Optional як enum: .some / .none

Optional як enum: .some / .none

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

Optional як enum: .some / .none

1. Optional як два стани

Коли ви тільки починаєте писати на Swift, Optional може здаватися дивною поведінкою змінної: то там ?, то там nil, то компілятор свариться: «Значення типу Optional потрібно розпакувати». На цьому етапі багатьом хочеться натиснути чарівну кнопку «Зробіть мені просто String, будь ласка».

Але Swift упертий — і саме в цьому його сила: він змушує вас явно описувати ситуації, коли значення є, і коли його немає. Найпростіше це зрозуміти, якщо перестати сприймати T? як «тип із питаннячком» і почати бачити в ньому тип-перелічення: у нього є два стани, і кожен має власний сенс.

Найважливіша думка цієї лекції звучить просто: Optional — це перелічення (enum). У нього є два варіанти:

  • .some(Wrapped) — значення є, і всередині лежить значення типу Wrapped
  • .none — значення немає

Це не метафора «ніби enum». Це реальна модель: Optional у стандартній бібліотеці саме так і мислиться — через розгалуження за .some/.none.

Щоб модель добре вклалася, уявіть Optional як коробку. Коробка або порожня (.none), або з предметом (.some(предмет)). Важливо: «порожня коробка» — це теж стан, а не помилка.

Ось невелика схема як ментальна картинка, щоб закріпити ідею:

stateDiagram-v2
    [*] --> Немає: .none (nil)
    [*] --> Є: .some(value)

    Є --> Немає: "значення втрачено або видалено"
    Немає --> Є: "отримано значення"

Суть у тому, що Optional — це не «тип, який іноді ламається». Це контейнер-стан, який може переходити з «порожньо» в «є значення» і назад.

2. Синтаксис Optional у Swift

T? і Optional<T> — це одне й те саме

У Swift є короткий запис T?, який ви бачите постійно: String?, Int?, [Book]? і так далі. Усередині мови це не окрема сутність, а зручне скорочення.

Повний запис має такий вигляд:

  • Int?Optional<Int>
  • String?Optional<String>

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

Ось мініприклад, який можна запустити просто зараз:

import Foundation

let a: Int? = 10
let b: Optional<Int> = 10

print(a as Any) // Optional(10)
print(b as Any) // Optional(10)

nil — це зручний запис для .none

Ви часто пишете nil, і це нормально. Але корисно розуміти, що nil — це «літерал відсутності», який відповідає стану .none.

Тобто змістово:

  • let x: Int? = nillet x: Optional<Int> = .none

Іноді корисно написати саме .none, щоб у вас у голові клацнуло: «Ага, я справді тримаю альтернативний стан, а не просто порожню змінну».

Мініприклад:

import Foundation

let c: Int? = nil
let d: Optional<Int> = .none

print(c as Any) // nil
print(d as Any) // nil

Явні .some(...) і .none: коли це допомагає

Зазвичай ви не будете постійно писати .some(10). Swift дозволяє просто написати 10, і він сам розуміє, що це значення всередині Optional. Але іноді явний запис дуже допомагає:

  • коли ви розбираєтеся в типах, особливо у складних виразах,
  • коли ви свідомо хочете показати, що тут Optional створено вручну,
  • коли ви читаєте помилки компілятора й бачите .some/.none у діагностиці.

Мікроприклад: функція може повернути книгу, а може й не повернути її — отже, результат має бути Book?.

import Foundation

struct Book {
    let id: Int
    let title: String
}

func findBook(byID id: Int) -> Book? {
    let sample = Book(id: 1, title: "Swift для сміливих")
    return id == sample.id ? .some(sample) : .none
}

Зверніть увагу: ми прямо повернули .some або .none, хоча могли написати коротше. Так ми «підсвітили» модель: функція повертає не книгу, а стан «книга є / книги немає».

Чому .none майже завжди вимагає контексту типу

Є одна тонкість, з якою ви обов’язково зіткнетеся: .none без контексту типу компіляторові часто незрозумілий.

Логіка компілятора дуже людська: «Гаразд, .none... але чого саме none? Int? String? Book?».

Тому .none добре працює там, де тип уже відомий:

import Foundation

let missingBook: Book? = .none
let missingText: String? = .none

print(missingBook as Any) // nil
print(missingText as Any) // nil

Тут усе гаразд, бо ліворуч ми явно сказали, що це Book? і String?.

А от якщо написати просто «нехай буде .none», компілятор може щиро розгубитися. І це не суворість заради суворості: тип .none справді залежить від контексту.

3. nil і «порожнє значення» — це різні смисли

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

Уявіть, що ви питаєте користувача: «Скільки книг у бібліотеці?». Можливі різні ситуації:

  • бібліотека справді порожня — це 0 книг (значення є)
  • ви не знаєте кількості — це nil (значення немає)

Порівняймо:

Ситуація Як виглядає Сенс
Значення є, але воно «нульове»
0, "", []
«Відповідь відома: вона така»
Значення немає
nil
«Відповіді немає / її не отримано / її не вдалося обчислити»

Ось мініприклад:

import Foundation

let countIsKnownButZero: Int? = 0
let countIsUnknown: Int? = nil

print(countIsKnownButZero == nil) // false
print(countIsUnknown == nil)      // true

Якщо ви зловите себе на думці «ну, nil — це як нуль, тільки для рядків», зупиніться. Це майже завжди логічний баг.

4. Практика: пошук у масиві та результат Book?

Зробімо ще один крок до реального застосунку. У нас є масив книг, поки що просто в памʼяті, і ми хочемо знайти книгу за id. Іноді книга знайдеться, іноді ні — отже, результат має бути Book?.

Так, пізніше ми запишемо це зручніше, але зараз нам важлива сама модель «може не бути».

import Foundation

struct Book {
    let id: Int
    let title: String
}

let books = [
    Book(id: 1, title: "Swift для сміливих"),
    Book(id: 2, title: "Алгоритми без сліз")
]

func findBook(byID id: Int, in books: [Book]) -> Book? {
    for book in books {
        if book.id == id { return .some(book) }
    }
    return .none
}

На рівні виведення це виглядає так — без розпакування. Його ми розбиратимемо в наступних лекціях:

import Foundation

let result1 = findBook(byID: 2, in: books)
let result2 = findBook(byID: 99, in: books)

print(result1 as Any) // Optional(Book(id: 2, title: "Алгоритми без сліз"))
print(result2 as Any) // nil

Тут важливо відчути: nil — це не «помилка виконання». Це нормальна відповідь: «книги з таким id немає».

Примітка про print, Any і Optional

У Swift print приймає значення типу Any. Тому, коли ви друкуєте Optional, результат іноді не такий, як ви очікуєте: може зʼявитися попередження або вивід «Optional(...)».

Для навчання є простий прийом: якщо ви хочете побачити Optional «як є», друкуйте as Any:

import Foundation

let x: Int? = 3
let y: Int? = nil

print(x as Any) // Optional(3)
print(y as Any) // nil

Не сприймайте це як «правильний стиль назавжди». Це радше навчальний ліхтарик: ним зручно посвітити в темний куток і зрозуміти, що у вас у руках справді Optional, а не «майже Int».

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

Помилка №1: сприймати Optional як «режим змінної», а не як окремий тип.
Коли ви думаєте «це просто String, але іноді nil», рука тягнеться поводитися з ним як зі звичайним String: викликати методи, склеювати, передавати далі. Компілятор починає сваритися, і здається, що він просто чіпляється. Насправді він каже: «У вас у руках не рядок, а два можливі стани». Звичка подумки проговорювати: «String? — це Optional-рядок» різко зменшує кількість конфліктів із компілятором.

Помилка №2: плутати nil і «порожнє значення».
Найпідступніша помилка — логічна. Застосунок компілюється, працює, але поводиться дивно: ви вважаєте, що «якщо значення порожнє, значить його немає». На жаль: 0 — це значення, "" — це значення, [] — це значення. А nil — це відсутність. Якщо ви змішали ці гілки, валідація й умови починають жити своїм життям.

Помилка №3: писати .none там, де компіляторові бракує типу, і дивуватися помилці.
Коли ви бачите, що .none «то працює, то не працює», це виглядає хаотично. Насправді правило просте: .none потребує контексту, бо «чого немає?» — важлива частина типу. Якщо контексту немає, додайте анотацію типу ліворуч або зробіть змінну чи параметр явно типу Optional.

Помилка №4: намагатися «швидше розпакувати» Optional, не розібравшись, що це за стан.
На цьому етапі багато хто починає використовувати примусове розпакування ! просто, щоб усе компілювалося. Це перетворює Optional із «безпечної моделі» на «міну сповільненої дії». Ми ще говоритимемо про способи правильно дістати значення, але вже зараз варто запамʼятати: якщо ви не можете словами пояснити, чому nil неможливий, ! використовувати не можна — ви не розпаковуєте, ви сподіваєтеся.

Помилка №5: вважати nil винятком, а не нормальним результатом деяких операцій.
readLine() може повернути nil. Int("abc") поверне nil. Пошук у словнику dict[key] може повернути nil. Це не «падіння застосунку», а нормальна відповідь: «даних немає». Якщо ви починаєте сприймати nil як «нештатну ситуацію», ви пишете код із зайвою панікою й зайвими костилями. Набагато корисніше сприймати це як звичайну гілку стану, яку ви зобовʼязані обробити.

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