Программирование SwiftОбработка ошибокРазработчик мобильных приложений на Swift

Практическая ситуация: бросающую функцию нужно представить как Result. Какой результат создаст этот код и з...

Практическая ситуация: бросающую функцию нужно представить как Result. Какой результат создаст этот код и за счёт какого механизма ошибка попадёт в failure?

enum LoadError: Error {
    case offline
}

func load() throws -> Int {
    throw LoadError.offline
}

let result = Result { try load() }
Проходите собеседования с ИИ помощником Hintsage

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

Код создаст Result<Int, Error>.failure(LoadError.offline). Инициализатор Result(catching:) немедленно выполняет переданную замыкающую функцию: возвращённое значение превращает в success, а выброшенную ошибку — в failure.

Тип ошибки не преобразуется в текст и не теряется: экземпляр LoadError.offline сохраняется внутри Result как значение типа Error.

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

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

Result решает другую задачу: представляет успех или ошибку обычным значением. Это удобно для API, где результат нужно сохранить, передать в замыкание, объединить с другими значениями или обработать позднее без немедленного перехвата исключения.

Инициализатор Result(catching:) связывает эти две модели и позволяет адаптировать существующую бросающую функцию к интерфейсу, возвращающему Result.

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

Допустим, библиотечная функция уже объявлена как throws, но слой приложения должен вернуть единое значение Result — например, для передачи результата в очередь, кэш или callback. Простое присваивание результата вызова невозможно: try load() либо возвращает Int, либо передаёт ошибку дальше.

Ручная адаптация через do-catch работает, но добавляет шаблонный код. При неаккуратной реализации можно случайно преобразовать ошибку в строку и потерять её тип, контекст и возможность pattern matching.

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

Выражение Result { try load() } использует Result(catching:). Замыкание выполняется сразу во время создания result, поэтому вызов load() не откладывается до последующего чтения значения.

Механизм эквивалентен следующей логике:

enum LoadError: Error { case offline } func load() throws -> Int { throw LoadError.offline } let result: Result<Int, Error> = Result { try load() } switch result { case .success(let value): print(value) case .failure(let error): print(error) }

Если замыкание завершится обычным return, будет создан .success(value). Если внутри произойдёт throw, будет создан .failure(error). В данном примере ошибка имеет динамический тип LoadError, хотя тип свойства failureError.

Преимущество такого подхода — сохранение исходной ошибки. Вызывающий код может проверить её тип:

if case .failure(LoadError.offline) = result { print("Нет соединения") }

Ограничение заключается в том, что Result(catching:) предназначен для ошибок, выбрасываемых из замыкания. Он не превращает аварийные состояния вроде fatalError в обычный failure.

Если вызывающий код всё равно сразу делает switch или вызывает try result.get(), использование Result может быть избыточным: в таком месте throws обычно проще. Result оправдан, когда состояние успеха или ошибки нужно хранить, передавать или комбинировать как данные.

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

Сервис загрузки данных уже предоставляет func fetch() throws -> Model, но слой аналитики должен передать итог операции в callback, который принимает Result<Model, Error>.

Рассматривались три варианта:

  • изменить fetch на возврат Result — это ломает существующих вызывающих и распространяет выбранную модель на весь API;
  • написать ручной do-catch — решение прозрачно, но содержит повторяющийся код адаптации;
  • использовать Result { try fetch() } — сохраняет исходную ошибку и не меняет API сервиса.

Выбран третий вариант. Результат создаётся в момент выполнения операции, а callback получает либо модель, либо исходный объект ошибки и может отдельно обработать сетевую, серверную или доменную причину сбоя. Это уменьшает связность между сервисом и слоем доставки результата.

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

  1. Вопрос: Какой тип ошибки будет доступен в result.failure, если load() выбрасывает конкретный тип LoadError?

    Ответ: Статический тип значения обычно будет Error, потому что Result(catching:) создаёт результат с типом ошибки Error. Однако сам объект внутри сохраняет динамический тип LoadError, поэтому его можно проверить приведением или pattern matching. Преобразование в Error не означает преобразование в строку и не уничтожает конкретное значение ошибки.

  2. Вопрос: Что произойдёт, если вызвать result.get() для результата .failure?

    Ответ: get() вернёт значение для .success, а для .failure выбросит сохранённую ошибку. Это позволяет преобразовать значение Result обратно в модель throws, но такой вызов требует try и подходящего обработчика. Если ошибка уже должна быть обработана через switch, вызов get() добавляет лишний переход между двумя моделями.

  3. Вопрос: Почему Result(catching:) не делает асинхронную операцию асинхронной автоматически?

    Ответ: Инициализатор принимает обычное синхронное бросающее замыкание и выполняет его в текущем контексте. Если операция имеет вид async throws, её нужно вызывать из async-контекста и отдельно выбрать модель представления результата — например, оставить async throws или написать собственный асинхронный адаптер. Сам Result хранит итоговое значение, но не управляет ожиданием, отменой или планированием задачи.