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()
Иногда хочется «правило на холодильник», поэтому зафиксируем разницу в одной таблице:
| Способ | Как выглядит | Когда обычно удобнее |
|---|---|---|
|
две ветки .success/.failure | когда в точке чтения нужно разное поведение (разные сообщения, разные действия) |
|
возвращает значение или бросает ошибку | когда вы уже пишете код в стиле 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‑кода и два способа чтения результата.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ