JavaRush /Курси /Swift SELF /Optional chaining ?. ...

Optional chaining ?. і nil‑coalescing ??

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

1. Ще два корисні оператори ?. і ??

Коли ви щойно опанували if let і guard let, виникає дуже природне відчуття: «Ну все, Optional переможено. Можна закривати тему й святкувати». Але в коді зʼявляються десятки дрібних дій: обчислити довжину введення, перевести команду в нижній регістр, перевірити префікс, підставити імʼя «Гість», якщо користувач мовчить. І раптом кожна така дрібниця потребує окремого if let.

І тут Swift каже: «Слухайте, я вас розумію. Давайте я дам короткий синтаксис для безпечного звернення до властивостей і методів та короткий синтаксис для осмисленого значення за замовчуванням». Ці інструменти — optional chaining ?. і nil‑coalescing ??. Головне — памʼятати, що вони не замінюють мислення. Вони лише прибирають зайвий шум там, де логіка вже зрозуміла.

Optional chaining ?.

Optional chaining читається майже як фраза українською: «спробуй звернутися до властивості чи методу, якщо значення є». Якщо значення немає (nil), вираз не падає і не робить зайвих рухів — він просто повертає nil. Тобто це безпечний доступ, який не потребує окремого if let для кожної дрібної дії.

Уявіть, що Optional — це коробка. У коробці або лежить річ, або порожньо. Звичайна крапка . — це «відкрий коробку й дістань річ». А ?. — це «якщо в коробці є річ, дістань її; якщо порожньо — добре, ідемо далі, просто результату немає».

Найбазовіший приклад — довжина введеного рядка:

import Foundation

let line: String? = readLine()
let length: Int? = line?.count

print(length as Any) // Optional(5) або nil

Зверніть увагу: length вийшов Int?, хоча count у рядка — це Int. Так і має бути: ми ж могли взагалі не отримати рядок.

Трохи реалістичніший варіант: користувач вводить команду, а ми хочемо перевести її в нижній регістр:

import Foundation

let command: String? = readLine()
let normalized: String? = command?.lowercased()

print(normalized as Any) // Optional("help") або nil

2. Правило: ?. повертає Optional

З optional chaining є правило, яке варто вивчити майже так само міцно, як таблицю множення. Але тут, на щастя, усе простіше і без шкільних флешбеків.

Якщо ліворуч стоїть T?, то будь-яке звернення через ?. дає результат типу U? — навіть якщо праворуч властивість або метод повертає не optional. У Swift це зроблено навмисно: ланцюжок має бути безпечним, а безпека виражається тим, що результат може бути відсутнім.

Це легко побачити на практиці: навіть якщо сам метод повертає звичайне значення, після ?. результат усе одно стає optional.

Зафіксуймо це маленькою таблицею — так мозок перестає сперечатися з компілятором:

Що маємо ліворуч Що пишемо Що отримуємо за типом Що це означає
String
name.count
Int
імʼя точно є
String?
name.count
❌ помилка компіляції «а раптом nil?»
String?
name?.count
Int?
довжина є, якщо імʼя є
String?
name?.count ?? 0
Int
довжина або значення за замовчуванням

І ще одна важлива деталь: якщо ви хочете побудувати ланцюжок із кількох звернень, то ?. зазвичай доводиться ставити на кожному кроці, де ліворуч optional. Це нормально: ви буквально позначаєте місця, де щось може піти не так.

Наприклад, ми хочемо взяти введення, перевести його в нижній регістр і перевірити префікс "add":

import Foundation

let isAddCommand: Bool = readLine()?.lowercased()?.hasPrefix("add") ?? false
print(isAddCommand) // true/false

Тут відбуваються три кроки, і кожен може не відбутися через nil, тому й результат до ?? — це Bool?.

3. Nil‑coalescing ??

Коли ви пишете something ?? defaultValue, ви кажете Swift: «Якщо ліворуч є значення — використай його. Якщо ліворуч nil — підстав значення за замовчуванням праворуч». Тобто ?? перетворює T? на звичайний T.

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

Найпростіший приклад, який і справді часто корисний: довжина введення. Якщо введення немає — довжина 0 виглядає логічно:

import Foundation

let inputLength: Int = readLine()?.count ?? 0
print(inputLength) // наприклад: 4

Інший приклад — команда. Якщо користувач нічого не ввів, ми можемо вважати, що він просить "help" (довідку).

import Foundation

let command: String = readLine()?.lowercased() ?? "help"
print("Команда: \(command)") // Команда: help

Корисна деталь: оператор ?? не повинен працювати всліпу з не-optional значеннями. Раніше в Swift іноді можна було випадково написати z ?? 7, де z взагалі не optional, і отримати дивну поведінку. Це вважали джерелом прихованих помилок, тому таку поведінку намагаються забороняти й діагностувати як типову проблему.

4. Звʼязка ?. + ??: безпека + значення за замовчуванням

Зараз ми зберемо «золоту звʼязку» цієї лекції. Зазвичай ви робите так:

  1. берете optional-значення (часто це readLine()),
  2. обережно звертаєтеся до нього через ?.,
  3. і наприкінці ставите ??, щоб отримати не optional результат, з яким зручно працювати далі.

Цей шаблон виглядає так:

something?.propertyOrMethod(...) ?? defaultValue

Давайте почнемо розвивати наш маленький консольний застосунок. Нехай він називається LibraryBuddy (поки що це просто «балаканка» в консолі, без колекцій і складних структур — вони будуть пізніше). Зараз наше завдання — читати команди користувача обережно й передбачувано.

Крок 1: функція читання команди

import Foundation

func readCommand() -> String {
    // Якщо введення немає — вважаємо, що попросили довідку
    return readLine()?.lowercased() ?? "help"
}

Тепер у main (коді на верхньому рівні) ми можемо використовувати це як «цеглинку»:

import Foundation

let cmd = readCommand()
print("Ви ввели: \(cmd)") // наприклад: "add" або "help"

Тут важливий момент у проєктуванні: функція повертає не optional, бо ми ухвалили рішення: «У разі відсутності введення поводимося так, ніби це help». Це рішення може бути хорошим або поганим — але воно, принаймні, явне.

Крок 2: швидкі перевірки без if let

Припустімо, ми хочемо розпізнати команди на кшталт "add ..." або "exit". Поки що без складного парсингу, просто як ідея:

import Foundation

let cmd = readLine()?.lowercased() ?? "help"

let isExit = (cmd == "exit")
let isAdd  = cmd.hasPrefix("add")

print("exit? \(isExit), add? \(isAdd)") // exit? false, add? true

Ми не використали ?. тут, бо cmd уже не optional: ми «закрили питання» значенням за замовчуванням.

5. Коли ?. кращий, ніж if let

Іноді if let виглядає чесніше, але для дуже маленької дії він виходить багатослівним. Optional chaining доречний там, де вам потрібен дрібний факт і ви готові прийняти nil або значення за замовчуванням як нормальний сценарій.

Наприклад, ми хочемо вивести підказку «ви ввели N символів». І якщо введення немає — просто вважаємо, що це 0.

Через if let це виглядало б так:

import Foundation

let line = readLine()
let length: Int

if let text = line {
    length = text.count
} else {
    length = 0
}

print("Довжина: \(length)") // Довжина: 0

А через ?. + ?? — так:

import Foundation

let length = readLine()?.count ?? 0
print("Довжина: \(length)") // Довжина: 0

Другий варіант коротший і водночас не ховає сенс. Це саме той випадок, коли «коротше» не означає «гірше».

6. Коли ?? може бути помилкою

Дуже легко підсісти на ?? як на «універсальний пластир». Зʼявилася помилка компіляції — «ага, додам ?? "" або ?? 0». Код компілюється, і здається, що все вдалося. А потім виявляється, що застосунок сприймає некоректне введення як норму й тихо починає працювати неправильно.

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

Порівняйте дві стратегії.

Стратегія «будь-якою ціною число»:

import Foundation

let age: Int = Int(readLine() ?? "") ?? 0
print("Вік: \(age)") // "Вік: 0" навіть якщо ввели "котик"

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

import Foundation

if let line = readLine(), let age = Int(line) {
    print("Вік: \(age)")
} else {
    print("Потрібно ввести ціле число") // чесно
}

Мораль проста: ?? доречний, коли значення за замовчуванням логічно правильне. Якщо воно маскує проблему — краще if let/guard let.

7. Корисні нюанси

Як обчислюється a?.b ?? c

Щоб не сприймати ?. як «магічну крапку із запитанням», корисно подумки уявляти, що робить Swift. Це не точний опис внутрішньої реалізації, але добра ментальна модель.

flowchart TD
    A["Є optional a (a: T?)"] --> B{"a == nil?"}
    B -- "так" --> C["a?.b стає nil"]
    C --> D["nil ?? c -> беремо c"]
    B -- "ні" --> E["Беремо значення a (T)"]
    E --> F["Обчислюємо b"]
    F --> G["Отримуємо результат (зазвичай теж Optional)"]
    G --> H{"результат nil?"}
    H -- "так" --> D
    H -- "ні" --> I["Беремо результат ліворуч від ??"]

Головна ідея: якщо на якомусь кроці ланцюжка вийшов nil, далі обчислення не йдуть. Ви або отримуєте nil, або — якщо є ?? — значення за замовчуванням.

Дужки і пріоритет ??

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

Наприклад, ми хочемо «довжину введення плюс один» — нехай це буде умовний «номер наступного символу»:

import Foundation

let x = (readLine()?.count ?? 0) + 1
print(x) // якщо введення немає — 1

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

8. Типові помилки під час роботи з ?. і ??

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

Помилка №1: очікувати, що ?. дасть не optional результат.
Дуже поширений сюрприз: ви пишете readLine()?.count і чекаєте Int, а отримуєте Int?. Але це логічно: якщо рядка не було, то й довжини немає. Лікується одним правилом: «?. зберігає optional-ність результату», а якщо потрібен звичайний тип — ставимо ?? з осмисленим значенням за замовчуванням.

Помилка №2: ставити значення за замовчуванням “на автоматі”, без сенсу.
?? "" і ?? 0 — найпопулярніші «заглушки». Проблема в тому, що вони можуть змінювати сенс програми. Для довжини рядка 0 — нормальне значення за замовчуванням. Для віку 0 — уже спірно. Для команди "" — майже напевно гірше, ніж "help". Кожне значення за замовчуванням має бути частиною логіки, а не спробою «аби компілювалося».

Помилка №3: робити занадто довгі ланцюжки й втрачати читабельність.
Технічно можна написати щось на кшталт readLine()?.lowercased()?.hasPrefix("add") ?? false — і це ще нормально. Але якщо ланцюжок починає розростатися, краще розбити його на проміжні let, щоб код читався лінійно. Optional chaining — інструмент компактності, а не змагання «хто вмістить найбільше в один рядок».

Помилка №4: використовувати ?? там, де потрібно явно обробити помилкове введення.
Якщо відсутність значення — це нормальна ситуація, ?? чудовий. Але якщо відсутність значення означає «користувач увів нісенітницю» або «дані критичні», то значення за замовчуванням починає маскувати проблему. У таких місцях краще повернутися до if let/guard let і зробити зрозумілу поведінку: повідомлення про помилку, повторне введення, вихід із функції — усе, що відповідає завданню.

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