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