Программирование SwiftOptionals и система типовРазработчик приложений на Swift

Как Swift формирует Optional вокруг результата try?, если throwing выражение уже возвращает Optional?

Как Swift формирует Optional вокруг результата try?, если throwing-выражение уже возвращает Optional?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Если throwing-выражение возвращает T?, оператор try? не добавляет ещё один уровень: результат также имеет тип T?. При успешном выполнении сохраняется возвращённое значение, включая nil; при ошибке результатом становится nil, поэтому успешный nil и ошибка становятся неразличимы.

Исторический контекст

Изначально применение try? к выражению, уже возвращающему Optional, могло приводить к вложенному типу T??. Это усложняло вывод типов и заставляло учитывать дополнительный уровень обёртки даже в случаях, когда он не нес практической информации.

Начиная со Swift 5 поведение изменено: try? не создаёт новый уровень Optional, если результат выражения уже является Optional. Такой подход сделал обработку необязательного результата более предсказуемой и устранил распространённые случаи случайного появления T??.

Постановка проблемы

Функция может использовать два разных способа сообщить об отсутствии результата: вернуть nil при корректном выполнении или выбросить ошибку при сбое. После применения try? оба состояния представлены одним и тем же значением nil.

Это допустимо, если вызывающему коду неважна причина отсутствия результата. Но при сетевой загрузке, чтении файла или обращении к базе данных такое упрощение может скрыть реальную ошибку и привести к неверной диагностике.

Подробное решение

Для выражения, возвращающего T, тип результата try?T?. Для выражения, возвращающего T?, тип результата остаётся T?, а не превращается в T??.

Механизм можно представить так: при успехе try? передаёт наружу значение, возвращённое выражением; при исключении подставляет nil. Поэтому для исходного результата T? оба пути могут дать одинаковый итог:

enum LoadError: Error { case offline } func load() throws -> String? { throw LoadError.offline } let value = try? load() // String?, значение nil let result = Result { try load() } // Result<String?, Error> print(value as Any) print(result)

В строке с try? ошибка преобразуется в nil. В Result сохраняется различие между успешным результатом success(nil) и ошибкой failure(...).

Важно, что Swift не «сплющивает» произвольное количество уже существующих уровней. Если выражение само имеет тип T??, try? не добавляет новый уровень, но существующая вложенность T?? остаётся. Иными словами, правило предотвращает дополнительное оборачивание, а не удаляет все Optional внутри результата.

Если нужно различать ошибку и успешное отсутствие значения, используйте do-catch, Result или другой явно моделирующий эти состояния тип. try? следует выбирать только тогда, когда любая ошибка действительно должна быть сведена к отсутствию значения.

Ситуация из практики

Кэш может вернуть строку, если запись найдена, и nil, если записи нет. Одновременно сетевой источник может выбросить ошибку при отсутствии соединения. Вызов через try? удобен для необязательной загрузки, но объединяет «запись не найдена» и «сервис недоступен»; его плюс — краткость, минус — потеря причины сбоя.

Вариант с do-catch сохраняет текст и тип ошибки и обычно лучше подходит для пользовательских сообщений и повторных попыток. Result<String?, Error> дополнительно позволяет передавать успех с отсутствующим значением как success(nil), не смешивая его с failure. Для слоя, отвечающего за кэширование и сеть, выбран бы Result, поскольку ему нужно отдельно обрабатывать отсутствие данных, повтор запроса и настоящую ошибку.

Что кандидаты часто упускают

  1. Удаляет ли try? все вложенные уровни Optional?

    Нет. Он не добавляет новый уровень, если результат уже является Optional, но не удаляет существующую вложенность. Поэтому выражение типа T?? после try? останется T??.

  2. Можно ли по результату try? определить, вернуло ли выражение успешный nil или выбросило ошибку?

    Нет. В обоих случаях результат имеет значение nil. Если это различие важно, нужно использовать do-catch, Result или собственный тип состояния, явно содержащий отдельные варианты для отсутствия данных и ошибки.

  3. Становится ли результат try? Optional при любом типе возвращаемого значения?

    Да, если исходный результат не был Optional: T превращается в T?. Но для уже Optional-результата дополнительная обёртка не создаётся. Например, результат функции throws -> Int будет Int?, а результат функции throws -> Int? — также Int?, хотя причины получения nil у них могут быть разными.