JavaRush /Курси /Swift SELF /Доступ за ключем повертає Optional

Доступ за ключем повертає Optional

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

1. Модель словника

Коли ви вперше бачите ages["Tom"] і замість Int отримуєте Int?, мимоволі виникає думка: «Що? Знову?». Насправді Swift просто піклується про вас — хоч і в доволі суворій формі.

Словник — це структура, у якій ключі не зобов’язані існувати. У масиві індекс майже завжди виглядає «підозрілим», але принаймні зрозуміло, що він має бути в межах діапазону. У словнику ж користувач може попросити ключ "Tom", а у ваших даних є лише "Ann" і "Bob". Що повертати? Нуль? Порожній рядок? «-1»? Усе це — костилі та домовленості, які ламаються вже на першому реальному проєкті.

Тому Swift каже так: «Якщо ключа може не бути, результат читання — Optional. Хочете значення — доведіть, що воно є». Це не занудство, а захист від дуже дорогих помилок.

Наочно модель виглядає так:

flowchart TD
    A["Є словник dict"] --> B["Запитуємо dict[key]"]
    B --> C{"Ключ існує?"}
    C -->|Так| D["Повертаємо .some(значення)"]
    C -->|Ні| E["Повертаємо nil (.none)"]

2. Як побачити nil і Optional під час друку

Коли ви друкуєте Optional, іноді виникає відчуття, ніби Swift спеціально хоче, щоб ви побачили Optional(...) і замислилися про сенс життя. У цьому навіть є педагогічний сенс.

Проблема в тому, що print(...) приймає аргументи типу Any, і коли ви передаєте туди optional, Swift ніби нагадує: ви передали Optional без розпакування.

Подивімося на простий приклад:

let ages: [String: Int] = ["Ann": 24, "Bob": 30]

let annAge = ages["Ann"]
let tomAge = ages["Tom"]

print(annAge as Any) // Optional(24)
print(tomAge as Any) // nil

Тут as Any — це не «магія», а просто спосіб сказати компілятору: «так, я свідомо друкую Optional; покажи мені, що там усередині».

3. Розпаковування значень зі словника

if let: базовий і найчитабельніший варіант

Тепер перейдемо до найпрактичнішого. Ви хочете отримати значення, якщо воно є, і нормально обробити ситуацію, якщо його немає. Для цього вам уже знайомий із попередніх тем інструмент — if let.

У контексті словника це читається майже як звичайне українське речення: «якщо за ключем є значення — візьми його».

let prices: [String: Int] = ["coffee": 250, "tea": 180]

if let teaPrice = prices["tea"] {
    print("Чай коштує \(teaPrice)")   // Чай коштує 180
} else {
    print("Чаю немає в прайс-листі")
}

Зверніть увагу на дві речі. По-перше, всередині гілки if змінна teaPrice уже має тип Int, а не Int?. Тобто Swift дозволив вам працювати без Optional там, де ви довели наявність значення. По-друге, код виходить чесним: ми не вдаємо, що ключ існує.

guard let: «перевірив і пішов далі»

Коли ви пишете функції, дуже швидко набридає вкладати логіку в «сходинки» if/else. Особливо якщо у вас є кілька перевірок поспіль: «чи є ключ, чи правильно введено, чи не порожній рядок…» У таких випадках Swift зазвичай пропонує guard let: якщо умови не виконано — виходимо одразу.

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

func printPrice(for item: String, in prices: [String: Int]) {
    guard let price = prices[item] else {
        print("Немає ціни для '\(item)'")
        return
    }

    print("'\(item)' коштує \(price)")
}

printPrice(for: "coffee", in: ["coffee": 250]) // 'coffee' коштує 250
printPrice(for: "cake", in: ["coffee": 250])   // Немає ціни для 'cake'

Зверніть увагу, як читається функція: спочатку перевірка, потім нормальний шлях. Це дуже зменшує шанс зламати логіку.

Не можна порівнювати й рахувати, поки значення — Optional

Дуже типовий сценарій для новачків: ви дістали значення зі словника і одразу хочете порівняти його з числом.

Наприклад, ви хочете перевірити, чи є товару більше нуля:

let stock: [String: Int] = ["pen": 3]
let count = stock["pen"]        // Int?
if count > 0 { ... }         // ❌ так не можна

Swift не дозволить порівняти Int? і Int, і це добре: порівняння «можливо відсутнього значення» — логічно дивна операція. У спільноті розробників Swift окремо обговорювали, що порівняння Optional може призводити до неочікуваних ефектів, тож краще уникати їх без явного розпакування.

Правильний варіант — розпакувати:

let stock: [String: Int] = ["pen": 3]

if let count = stock["pen"] {
    if count > 0 {
        print("Є в наявності: \(count)")  // Є в наявності: 3
    } else {
        print("Немає в наявності")
    }
} else {
    print("Немає такого товару")
}

Так, коду стане трохи більше. Зате логіка чесна: «немає ключа» й «нульовий залишок» — різні випадки.

Перевірка != nil + повторне читання — поганий стиль

Іноді пишуть так: «якщо ключ існує, тоді прочитаємо значення».

// ❌ приклад поганого стилю (але компілюється)
if catalog["b1"] != nil {
    print(catalog["b1"]!) // два звернення + force unwrap
}

Тут одразу дві проблеми. Перша — ви читаєте словник двічі, а це зайва робота і зайвий шанс помилитися під час рефакторингу. Друга — ви все одно використовуєте !, тобто якщо код колись зміниться або ключ зміниться, ви отримаєте падіння.

Правильніше — «одним рухом»:

if let title = catalog["b1"] {
    print(title)
}

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

4. Значення за замовчуванням і !: де межа між зручністю та крихкістю

??: коли значення за замовчуванням справді має сенс

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

Ось тут ідеально підходить ??:

let views: [String: Int] = ["home": 120, "profile": 5]

let aboutViews = views["about"] ?? 0
print("Перегляди сторінки «Про нас»: \(aboutViews)") // Перегляди сторінки «Про нас»: 0

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

Чому dict[key]! — погана ідея (майже завжди)

Іноді здається: «Та ну, я точно знаю, що ключ є». І рука тягнеться до !:

let age = ages["Ann"]! // 

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

Якщо ви все-таки використовуєте !, нехай це буде свідомо: наприклад, у навчальному коді, де ви жорстко контролюєте дані, або в ситуації, де відсутність ключа означає баг розробника, а не помилку користувача. Але в більшості випадків для словника це занадто крихко.

5. Мініпроєкт LibraryMini: пошук книги за ID

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

Нехай у нас є словник catalog, де ключ — це ID книги, а значення — її назва. Користувач вводить ID, а ми друкуємо назву або повідомляємо, що книги немає.

Спочатку — дані та введення:

import Foundation

let catalog: [String: String] = [
    "b1": "Swift для початківців",
    "b2": "Алгоритми 101",
    "b3": "Пригоди CLI"
]

print("Введіть ID книги (b1/b2/b3):", terminator: " ")
let id = readLine() ?? ""

Тепер найважливіше — безпечне читання за ключем:

if let title = catalog[id] {
    print("Назва: \(title)")
} else {
    print("Немає книги з ID '\(id)'")
}

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

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

let safeTitle = catalog[id] ?? "<невідома книга>"
print("Назва: \(safeTitle)")

Зверніть увагу: тут ми втрачаємо можливість розрізняти «книги справді немає» і «книга є, але називається <невідома книга>» (якби така назва раптом існувала). У реальних застосунках зазвичай корисніший перший варіант із if let, тому що він чесніший.

6. Шпаргалка: який спосіб розпаковування вибрати

Коли варіантів кілька, новачкам важливо не заучувати назви, а бачити сенс. Давайте зафіксуємо це в одній таблиці.

Ситуація Що писати Чому це підходить
«Якщо значення є — використовую, якщо ні — роблю інше»
if let value = dict[key] { ... } else { ... }
Найчесніший і найчитабельніший контроль потоку
«Я всередині функції, і якщо значення немає, хочу одразу вийти»
guard let value = dict[key] else { return }
Ранній вихід робить основний шлях коду прямим
«Відсутність значення — це нормально, беру значення за замовчуванням»
let v = dict[key] ?? defaultValue
Прибирає розгалуження, але важливо вибрати змістовне значення за замовчуванням
«Я на 100 % впевнений, що ключ є, інакше це баг»
dict[key]!
Працює, але може впасти — обережно й рідко

Ще одна тонкість щодо print: якщо ви хочете свідомо друкувати Optional, краще писати as Any, щоб явно показати намір. Це пов’язано з тим, що Optional може неявно потрапляти в Any, і Swift прагне попереджати про такі місця.

7. Типові помилки під час читання зі словника

Помилка № 1: очікувати, що dict[key] повертає «звичайне значення», а не Optional.
Це зазвичай проявляється так: студент пише let age: Int = ages["Ann"] і отримує помилку типів. Лікується не «приведенням типів» і не !, а розумінням моделі: ключ може бути відсутнім, отже результат — Int?, і його потрібно розпакувати через if let, guard let або змістовне ??.

Помилка № 2: лікувати всі Optional через !, бо «так компілюється».
Так, компілюється. Але ціна — падіння програми в будь-який момент, коли ключ раптово не знайдено, а словники часто живуть поруч із введенням користувача. Правильний підхід — спочатку вирішити, що для вас означає відсутність ключа: це помилка, «немає даних», значення за замовчуванням чи привід показати підказку. І вже під це обрати if let / guard let / ??.

Помилка № 3: змішувати «немає ключа» і «значення дорівнює 0/порожньо».
Класика: зберігати вік, а за відсутності ключа підставляти 0, після чого вік 0 починає поводитися як «дані існують». Це логічна помилка, а не синтаксична. Нормальний стиль: якщо відсутність — окремий стан, залишайте nil і обробляйте його розгалуженням, а не підміняйте «немає даних» на «дані дорівнюють нулю».

Помилка № 4: намагатися порівнювати Optional напряму (dict[key] > 0) або робити обчислення, не розпакувавши значення.
Swift забороняє це не зі шкідливості, а щоб ви не виконували беззмістовні операції над «можливою відсутністю». Важливо прийняти правило: порівняння й арифметика виконуються над звичайними значеннями (Int, Double), тож спочатку — розпакування. Додатково корисно пам’ятати, що порівняння Optional може призводити до неочікуваних семантик, тому мова історично посилила обмеження в цій сфері.

Помилка № 5: «перевірив != nil, потім ще раз дістав значення».
Такий код майже завжди виходить більш крихким: ви робите два читання, а ще часто закінчуєте !. Набагато простіше й безпечніше одразу використовувати if let, який одночасно перевіряє наявність і дає вам не-Optional значення в тілі гілки.

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