JavaRush /Курси /Swift SELF /try

try ? — перетворюємо помилку на nil, коли це доречно

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

1. Що таке try? і навіщо він потрібен

Іноді новачкам здається, що будь-який throw треба обов’язково «зловити» й обробити, інакше програма образиться й піде в монастир. Насправді все навпаки: частина помилок нам справді важлива (наприклад, користувач увів команду в неправильному форматі), а частина — це лише невдала спроба отримати значення, коли причина не має значення. Саме для такого випадку й існує try?.

З погляду проєктування коду це дуже корисний інструмент: try? дає змогу перетворити «операція може викинути помилку» на «операція може не дати значення». Тобто ми переносимо задачу зі світу помилок (throws) у світ опціоналів (Optional). Але, як і в будь-якого «спрощувача коду», за це є ціна: ми втрачаємо причину помилки й залишаємо тільки факт «успіх/неуспіх».

Як працює try?

Якщо уявити do/catch як повноцінний «пункт розгляду» — із протоколом, свідками та адвокатом, — то try? є його мініверсією: «давайте спробуємо, а якщо щось піде не так — мовчки повернемо nil». Важливо: try? не дає вам доступу до самої помилки. Він не «обробляє» її в логічному сенсі — він просто перетворює її на відсутність значення.

Механіка проста:

  • вираз виконався успішно → ви отримуєте значення;
  • усередині стався throw → ви отримуєте nil.

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


enum ParseIntError: Error { case invalidFormat }

func parseInt(_ text: String) throws -> Int {
    guard let n = Int(text) else { throw ParseIntError.invalidFormat }
    return n
}

let a = try? parseInt("42")
let b = try? parseInt("сорок два")

print(a as Any) // Optional(42)
print(b as Any) // nil

Зверніть увагу на головне: ніякого catch тут немає. Ми свідомо сказали: «причина невдачі мене зараз не цікавить».

Як змінюється тип результату: з T у T?

Дуже часта пастка для новачків — try? змінює тип. Навіть якщо функція повертає звичайний Int, після try? ви отримуєте Int?. Далі ваш код має це враховувати: або розпаковувати значення через if let/guard let, або підставляти значення за замовчуванням через ??, або зберігати його як optional і спокійно працювати далі.

Ось коротка таблиця-нагадування:

Що повертає функція Звичайний виклик із try Виклик через try?
Int
Int (або виняток)
Int?
String
String (або виняток)
String?
Void
Void (або виняток) Void? (зазвичай це безглуздо)

Покажімо це на коді:

enum ParseIntError: Error { case invalidFormat }

func parseInt(_ text: String) throws -> Int {
    guard let n = Int(text) else { throw ParseIntError.invalidFormat }
    return n
}

let value: Int? = try? parseInt("100")
print(value as Any) // Optional(100)

Якщо ви й далі подумки сприймаєте value як «точно Int», ви майже напевно потрапите в наступну пастку: спробуєте виконати з ним арифметику, порівняння чи форматування — і компілятор чемно нагадає, що «у вас тут Optional, розпакуйте його».

2. Базові патерни використання try?

try? + if let / guard let

Коли помилка перетворюється на nil, логічно відразу скористатися звичним механізмом розпакування optional-значень. Це один із найчитабельніших сценаріїв для try?: ми хочемо виконати дію лише тоді, коли значення справді вдалося отримати. Якщо ні — просто нічого не робимо (або застосовуємо резервний варіант), без детального аналізу причин.

Приклад з if let:

enum ParseIntError: Error { case invalidFormat }

func parseInt(_ text: String) throws -> Int {
    guard let n = Int(text) else { throw ParseIntError.invalidFormat }
    return n
}

if let n = try? parseInt("7") {
    print("Квадрат =", n * n) // Квадрат = 49
}

А тепер той самий зміст, але через guard let усередині функції. Часто такий варіант виглядає ще охайніше, тому що помилкова гілка йде вниз, а основний сценарій читається прямолінійно:

enum ParseIntError: Error { case invalidFormat }

func parseInt(_ text: String) throws -> Int {
    guard let n = Int(text) else { throw ParseIntError.invalidFormat }
    return n
}

func printSquareIfPossible(_ text: String) {
    guard let n = try? parseInt(text) else { return }
    print("Квадрат =", n * n) // для "7" -> Квадрат = 49
}

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

try? + ?? — значення за замовчуванням замість розборів

Друга дуже популярна зв’язка — try? з оператором ??. Це майже як запасний акумулятор: якщо вдалося — використовуємо результат, якщо ні — підставляємо значення за замовчуванням. Такий підхід часто застосовують для некритичних параметрів: ліміту, сортування, необов’язкового числа в конфігу, необов’язкового поля введення.

Приклад:

enum ParseIntError: Error { case invalidFormat }

func parseInt(_ text: String) throws -> Int {
    guard let n = Int(text) else { throw ParseIntError.invalidFormat }
    return n
}

let input = "abc"
let limit = (try? parseInt(input)) ?? 10
print(limit) // 10

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

Для закріплення можна подивитися на це як на мініблок-схему:

flowchart TD
    A["спроба parseInt(text)"] -->|успіх| B["отримуємо Int"]
    A -->|помилка| C["отримуємо nil"]
    C --> D["?? беремо значення за замовчуванням"]
    B --> E["використовуємо значення"]
    D --> E

3. Коли try? не підходить

Плата за try?: ми втрачаємо причину помилки

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

Уявіть ситуацію: ми хочемо отримати нікнейм користувача. Коректним результатом може бути nil — нікнейма немає. Але функція може й викинути помилку, якщо сервіс недоступний. Якщо ми застосуємо try?, обидва випадки перетворяться на nil, і далі код не зможе відрізнити «у користувача немає ніку» від «сервіс упав».

Приклад:

enum NicknameError: Error { case serviceUnavailable }

func findNickname(for name: String) throws -> String? {
    if name == "Ann" { return "Ace" }
    if name == "Error" { throw NicknameError.serviceUnavailable }
    return nil
}

let n1 = try? findNickname(for: "Ann")
let n2 = try? findNickname(for: "Bob")
let n3 = try? findNickname(for: "Error")

print(n1 as Any) // Optional(Optional("Ace"))
print(n2 as Any) // Optional(nil)
print(n3 as Any) // nil (помилка стала nil)

Сенс прикладу не в тому, щоб запам’ятати ці значення, а в тому, щоб відчути проблему: якщо причина важлива — використовуйте do/catch. try? доречний рівно тоді, коли ви свідомо погоджуєтеся з тим, що всі причини невдачі для вас однакові.

try? і Optional-результати: чому «подвійний Optional» трапляється рідше

У старіших версіях Swift try? легко породжував вкладені optional-типи на кшталт T??, якщо початковий вираз уже повертав T?. Це було неприємно для новачків: ви наче зробили все красиво й безпечно, а отримали Optional(Optional(...)) і два рівні розпакування.

Починаючи зі Swift 5, мова змінила поведінку try?: вона намагається не додавати зайвий рівень optional там, де вираз уже optional, і обирає мінімальну вкладеність, щоб результат і надалі міг означати «помилка → nil». Практичний наслідок: якщо функція повертає String?, то try? найчастіше дасть вам просто String?, а не String??.

Проте два рівні optional усе ще можуть з’явитися, наприклад, якщо ви самі десь створили «подвійний optional» (Int??) або змішали try? з optional chaining чи кастами у складному виразі. Тому загальний принцип простий: якщо ви бачите в налагодженні Optional(Optional(...)), не лякайтеся — це не помилка компілятора, а типи чесно показують, що ви зібрали.

4. Приклад: опціональні поля під час введення книги

Перенесімо це в практику: ми пишемо консольну програму й часто читаємо рядки через readLine(). Частину полів користувач може не хотіти заповнювати: наприклад, рік видання книги або рейтинг. Нам хочеться, щоб програма не падала й не перетворювалася на «допит із пристрастю», якщо поле некритичне. Ось тут try? і стає по-справжньому корисним.

Зробімо маленький фрагмент нашого застосунку: введення даних про книгу. Домовмося, що title обов’язковий, а year — опціональний. Рік ми перевіряємо й кидаємо помилку, якщо число не схоже на рік.

Крок 1: функція парсингу року:

enum YearError: Error { case outOfRange }

func parseYear(_ text: String) throws -> Int {
    guard let y = Int(text), (1450...2100).contains(y) else { throw YearError.outOfRange }
    return y
}

Крок 2: читаємо рік як «може бути»:

let yearLine = "1999"   // уявіть, що це readLine() ?? ""
let year: Int? = try? parseYear(yearLine)

print(year as Any) // Optional(1999)

Крок 3: додамо поведінку «порожній рядок = користувач пропустив поле»:

let yearLine = "" // користувач натиснув Enter
let year: Int? = yearLine.isEmpty ? nil : (try? parseYear(yearLine))

print(year as Any) // nil

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

Якщо ж за правилами вашого застосунку «неправильний рік» має переривати операцію додавання книги й вимагати повторного введення, то try? уже не найкращий вибір: вам знадобиться do/catch, щоб відрізнити помилку від порожнього введення й показати зрозуміле повідомлення. Але це рішення важливо приймати свідомо: try? добрий не тим, що «коротший», а тим, що чесно змінює модель задачі на optional.

5. Типові помилки під час використання try?

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

Помилка №1: використовувати try? там, де важливі різні причини.
Це зазвичай проявляється так: ви спершу робите try?, отримуєте nil, а потім намагаєтеся пояснити користувачеві, що сталося. Але ви вже не знаєте: «файл не знайдено», «немає прав», «пошкоджений формат», «не той рік», «не той діапазон». У результаті повідомлення стають надто загальними, а діагностика — марною. Якщо причина впливає на поведінку або текст повідомлення, значить try? був передчасним, і тут потрібен do/catch.

Помилка №2: ховати try? занадто глибоко.
Іноді розробник робить «зручну» функцію на кшталт loadSomethingOrNil() і всередині мовчки приглушує помилку через try?, хоча на вищому рівні ця помилка могла б бути важливою. У підсумку зверху залишається лише nil, і вже не з’являється жодного шансу коректно повідомити користувача або залогувати ситуацію. Добре правило: приглушуйте помилку максимально близько до місця, де ви справді ухвалили рішення «причина не важлива».

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

Помилка №4: застосовувати try? до операцій, у яких немає сенсу в nil.
Іноді трапляється дивний код: let _ = try? doImportantThing() — і все. Якщо ви не використовуєте результат і не змінюєте поведінку при nil, ви просто мовчки ковтаєте можливу проблему. Це майже завжди погіршує підтримку програми: баги стають «тихими». try? має сенс, коли nil — це реальна, осмислена гілка логіки, а не просто спосіб прибрати червоні підкреслення.

Помилка №5: вважати try? «безпечнішою версією try».
Насправді це інший контракт. try + do/catch зберігає керування й інформацію про помилку. try? зберігає лише керування, але відкидає інформацію. Іноді це правильна плата за простоту, іноді — прямий шлях до «чому воно не працює, ніхто не знає, але наче не падає».

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