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? = nil ⇔ let 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 (значення немає)
Порівняймо:
| Ситуація | Як виглядає | Сенс |
|---|---|---|
| Значення є, але воно «нульове» | |
«Відповідь відома: вона така» |
| Значення немає | |
«Відповіді немає / її не отримано / її не вдалося обчислити» |
Ось мініприклад:
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 як «нештатну ситуацію», ви пишете код із зайвою панікою й зайвими костилями. Набагато корисніше сприймати це як звичайну гілку стану, яку ви зобовʼязані обробити.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ