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 binding. Это один из самых читабельных сценариев для try?: мы хотим выполнить действие, только если значение получилось. Если нет — просто не делаем ничего особенного (или делаем общий fallback), без детального анализа причин.

Пример с 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? с nil-coalescing оператором ??. Это почти как “поставить запасной аккумулятор”: если получилось — используем результат, если нет — подставляем дефолтное значение. Такой код часто используется для “не критичных” параметров: лимит, сортировка, необязательное число в конфиге, опциональное поле ввода.

Пример:

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["try? parseInt(text)"] -->|успех| B["получили Int"]
    A -->|throw| 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? сохраняет только управление, но выкидывает информацию. Иногда это правильная цена за простоту, иногда — прямой путь к “почему оно не работает, никто не знает, но вроде не падает”.

1
Задача
Swift SELF, 30 уровень, 0 лекция
Недоступна
Квадрат без паники
Квадрат без паники
1
Задача
Swift SELF, 30 уровень, 0 лекция
Недоступна
Лимит витрины
Лимит витрины
1
Задача
Swift SELF, 30 уровень, 0 лекция
Недоступна
Три жетона
Три жетона
1
Задача
Swift SELF, 30 уровень, 0 лекция
Недоступна
Тайминг матча
Тайминг матча
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ