JavaRush /Курсы /Swift SELF /Result { try … }, get(), switch result

Result { try … }, get(), switch result

Swift SELF
31 уровень , 1 лекция
Открыта

1. Иногда одного throws недостаточно

Когда вы только освоили throws, возникает приятное чувство: «Ну всё, теперь я профессионально падаю». И это действительно мощный инструмент — но у него есть особенность: ошибка живёт в потоке управления. То есть либо функция вернула значение, либо «выпрыгнула» наружу через throw.

А теперь представьте более «прикладной» сценарий, который регулярно появляется в CLI-приложениях (и в нашем учебном LibraryCLI — тоже). Пользователь ввёл команду, мы пытаемся распарсить аргументы, и у нас есть два желания одновременно: во‑первых, не уронить программу, во‑вторых, сохранить результат попытки (успех или ошибку) как данные. Например, чтобы сложить несколько результатов в массив и потом вывести красивый отчёт.

Вот тут throws становится неудобен: он не «хранит исход», он просто меняет траекторию выполнения. Для хранения исхода нам нужен контейнер, и таким контейнером становится Result.

2. Result { try … }: упаковываем throwing‑код

Result — это enum из стандартной библиотеки: он хранит либо .success(...), либо .failure(...). Самое приятное начинается, когда у вас уже есть функция, которая throws, а вам нужно превратить её вызов в значение Result.

Для этого в Swift есть удобный конструктор: Result { try ... }. На самом деле это init(catching:), который выполняет throwing‑замыкание и кладёт исход внутрь Result. Именно такая форма показана в дизайне стандартной библиотеки.

Минимальный пример: было throws, стало Result

Начнём с маленького и максимально «школьного» примера: деление, которое не любит ноль (и это взаимно).

import Foundation

enum MathError: Error {
    case divisionByZero
}

func divide(_ a: Int, by b: Int) throws -> Int {
    guard b != 0 else { throw MathError.divisionByZero }
    return a / b
}

let r = Result { try divide(10, by: 2) }
print(r) // success(5)

Ключевая мысль: r — это обычное значение, его можно передать дальше, сохранить, положить в массив, вернуть из другой функции и т.д.

Важный нюанс типа: почему там часто появляется Error

Форма Result { try ... } (то есть init(catching:)) устроена так, что она создаёт Result<Success, Error> (в Swift 6 вы часто увидите запись any Error, но по смыслу это «какая-то ошибка, соответствующая Error»). Это видно прямо в описании init(catching:) для Result, где он доступен при Failure == Swift.Error.

Практическое следствие простое: если ваша throwing‑функция бросает конкретную ошибку (например, ParseError), то Result { try ... } всё равно упакует её как Error (обобщённо). На этом этапе курса это даже удобно: мы ещё не строим сложные «деревья ошибок», нам важно научиться механике упаковки и чтения результата.

Result { ... } выполняется сразу

Очень частая ошибка новичка — подсознательно думать, что Result { ... } создаёт «объект операции», который выполнится позже. Нет: он выполняется сразу, как обычное выражение.

Давайте специально распечатаем порядок:

import Foundation

enum DemoError: Error { case boom }

func risky() throws -> Int {
    print("risky() is running")     // risky() is running
    throw DemoError.boom
}

print("before")                     // before
let r = Result { try risky() }
print("after")                      // after
print(r)                            // failure(boom)

Если вы увидели в консоли risky() is running между before и after, значит всё честно: выполнение было немедленным.

3. Чтение Result через switch

Когда у нас в руках Result, мы должны его «распаковать». И самый честный способ — сделать switch по двум кейсам: .success и .failure. Этот подход особенно хорош в CLI, потому что в .success мы продолжаем сценарий, а в .failure печатаем понятную ошибку пользователю и решаем, что делать дальше.

Чтобы это выглядело как нормальная программа, а не как «пример ради примера», представим кусок нашего LibraryCLI: пользователь вводит год издания книги, и мы хотим превратить строку в Int (и не притворяться, что "abc" — это тоже год).

Сделаем функцию, которая throws:

import Foundation

enum ParseYearError: Error {
    case empty
    case notANumber
}

func parseYear(_ text: String) throws -> Int {
    guard !text.isEmpty else { throw ParseYearError.empty }
    guard let year = Int(text) else { throw ParseYearError.notANumber }
    return year
}

А теперь — упаковка в Result и чтение через switch:

import Foundation

let yearResult = Result { try parseYear("2020") }

switch yearResult {
case .success(let year):
    print("Год принят: \(year)")        // Год принят: 2020
case .failure(let error):
    print("Ошибка года: \(error)")
}

Здесь важно почувствовать стиль: switch буквально заставляет вас честно обработать оба исхода. В этом его сила: меньше шансов «случайно забыть про ошибку».

Небольшая схема

Иногда полезно один раз увидеть процесс в виде картинки — и потом не путаться.

flowchart TD
    A["throwing-вызов
try parseYear(text)"] --> B["Result { ... }"] B --> C{result} C -->|.success| D["используем значение
year"] C -->|.failure| E["показываем ошибку
error"]

4. Чтение Result через get()

switch — отличный способ, но иногда код уже написан в стиле throws. Например, у вас есть функция, которая парсит команду целиком и сама throws, а внутри вы временно храните куски как Result. В таком случае удобно «вернуть всё как было» — достать значение из Result, а если там ошибка, то снова превратить её в throw.

Для этого у Result есть метод get(): он возвращает Success, а если внутри .failure, то бросает эту ошибку. Поэтому get() всегда пишется через try. Это прямо зафиксировано в контракте стандартной библиотеки: “Returns the success value… Throws: the failure value…”.

Пример: get() в паре с do/catch

import Foundation

let yearResult = Result { try parseYear("x") }

do {
    let year = try yearResult.get()
    print("Год принят: \(year)")
} catch {
    print("Не смогли прочитать год: \(error)") // Не смогли прочитать год: notANumber
}

Смысл: Result можно читать либо как «ветвление значений» (switch), либо как «почти throwing‑функцию» (get() + do/catch).

Таблица: что выбирать — switch или get()

Иногда хочется «правило на холодильник», поэтому зафиксируем разницу в одной таблице:

Способ Как выглядит Когда обычно удобнее
switch result
две ветки .success/.failure когда в точке чтения нужно разное поведение (разные сообщения, разные действия)
try result.get()
возвращает значение или бросает ошибку когда вы уже пишете код в стиле throws и хотите встроить Result в существующий do/catch

Заметьте, здесь нет «лучше/хуже». Есть «читабельнее в данном месте». В хорошем коде встречаются оба стиля.

5. Практика: Result в LibraryCLI

Сейчас соберём маленький фрагмент, похожий на реальный кусок CLI. Пусть у нас есть команда:

add "Some Title" 2020

Мы пока не строим сложный парсер команд (это отдельная большая история), но уже можем сделать аккуратную заготовку: отдельная функция парсит год, бросает ошибки, а верхний уровень решает, что писать пользователю.

Сущность команды

Мы ещё не углубляемся в богатый домен — нам достаточно структуры с двумя полями.

import Foundation

struct AddBookCommand {
    let title: String
    let year: Int
}

Throwing‑парсер команды

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

import Foundation

enum AddCommandError: Error {
    case notEnoughArgs
    case emptyTitle
    case invalidYear(Error)
}

func parseAddCommand(_ tokens: [String]) throws -> AddBookCommand {
    guard tokens.count >= 3 else { throw AddCommandError.notEnoughArgs }
    guard !tokens[1].isEmpty else { throw AddCommandError.emptyTitle }

    let yearResult = Result { try parseYear(tokens[2]) }
    let year = try yearResult.get()   // может бросить

    return AddBookCommand(title: tokens[1], year: year)
}

Здесь мы намеренно использовали и Result { ... }, и get() в одном месте: parseYear у нас throwing, но нам было удобно сделать промежуточный Result, чтобы подчеркнуть: «вот тут потенциальный провал». Да, мы могли написать let year = try parseYear(...) — и это тоже нормально. Но пример показывает механику get() на знакомом материале.

Верхний уровень: превращаем throws в Result

А теперь представим, что у нас «верх программы» не хочет throw (потому что CLI должен общаться с пользователем, а не падать). Он хочет получить исход как значение и красиво обработать.

Вот тут Result { try ... } идеально ложится:

import Foundation

let tokens = ["add", "Swift in Depth", "x"]

let cmdResult = Result { try parseAddCommand(tokens) }

switch cmdResult {
case .success(let cmd):
    print("Добавляем книгу: \(cmd.title) (\(cmd.year))")
case .failure(let error):
    print("Ошибка команды add: \(error)")
}

Мы получили аккуратную развилку: успех — идём дальше, ошибка — печатаем. Заметьте, мы не делали do/catch в этом месте, хотя могли бы. Просто switch читается в CLI‑сценариях очень прямолинейно.

Ещё один практичный приём: хранить результаты нескольких попыток

В CLI и в реальных программах часто бывает ситуация «попробуем обработать много элементов и соберём статистику». Например, пользователь ввёл несколько значений, и нам хочется показать: какие распарсились, а какие нет. С throws это неудобно: первая ошибка может остановить весь цикл (если вы специально не ловите её каждый раз). А Result позволяет хранить всё как данные.

Сделаем мини‑пример: пользователь ввёл годы через пробел.

import Foundation

let inputs = ["1999", "x", "", "2020"]
let results = inputs.map { text in
    Result { try parseYear(text) }
}

for r in results {
    switch r {
    case .success(let year):
        print("OK: \(year)")
    case .failure:
        print("FAIL")
    }
}

Обратите внимание, как спокойно это ложится в коллекции: results — это просто массив, и каждый элемент сам хранит свой исход.

6. Типичные ошибки

Ошибка №1: ожидать, что Result { ... } выполнится «потом».
Это частое заблуждение, особенно если вы пришли из мира, где «объект задачи» и «выполнение задачи» — разные сущности. Здесь всё проще: Result { try ... } выполняет код сразу и мгновенно превращает исход в значение. Если внутри были print или изменения переменных, они произойдут прямо при создании Result.

Ошибка №2: писать result.get() без try.
Метод get() устроен так, что он возвращает успех, но при ошибке бросает её наружу. Поэтому компилятор требует try — и это не бюрократия, а подсказка: «ты сейчас можешь уронить поток выполнения, будь внимателен». Контракт get() как throwing‑метода прямо описан в стандартной библиотеке.

Ошибка №3: использовать try! result.get() “чтобы не писать do/catch”.
Иногда это выглядит как «ну мне же точно придёт успех». На практике это превращает управляемую ошибку (которую вы могли показать пользователю) в аварийное завершение программы. Для CLI это почти всегда плохой UX: пользователь ввёл "x", а приложение упало так, будто это личное оскорбление.

Ошибка №4: печатать ошибку внутри функции и одновременно возвращать Result.
Это смешивание ответственностей: функция, которая возвращает Result, должна просто вернуть данные. Печать — это задача более верхнего уровня (CLI-слоя). Иначе вы получите «двойные сообщения»: функция распечатала ошибку, а вызывающий код тоже распечатал, потому что увидел .failure.

Ошибка №5: пытаться сразу сделать Result<Success, MyError> через Result { try ... } и удивляться типам.
Result { try ... } создаёт результат с ошибкой типа Error (обобщённо), потому что это init(catching:) для случая Failure == Error. Если вам принципиально нужен конкретный тип ошибки, это решается другими приёмами (мы подойдём к этому аккуратно следующим шагом курса), но на текущем этапе важно сначала уверенно освоить упаковку throwing‑кода и два способа чтения результата.

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