1. Две траектории выполнения: значение или ошибка
Когда мы пишем обычную функцию, мозг автоматически ожидает, что она «просто вернёт результат». Но реальная жизнь (и пользовательский ввод) регулярно напоминают: результат может быть невозможен. И вот тут Swift предлагает цивилизованный вариант: функция может завершиться по двум траекториям — либо вернуть значение, либо «выстрелить» ошибкой.
Если раньше мы часто делали так: «не получилось → вернём nil», то теперь добавляется другой контракт: «не получилось → вернём причину через throw». Причина не теряется, она не превращается в магический nil, и её можно обработать на уровне, где есть смысл принять решение.
Наглядно это можно представить так:
flowchart TD
A[Вызов функции] --> B{Внутри всё ок?}
B -->|Да| C[return результат]
B -->|Нет| D[throw ошибка]
2. throws в сигнатуре: объявляем «опасную» функцию
Слово throws в Swift — это не «декорация», а часть контракта функции. Оно сообщает вызывающему коду: «Смотри, тут есть риск: я либо верну нормальный результат, либо брошу ошибку». И компилятор относится к этому максимально серьёзно: если вы видите throws, дальше в коде почти неизбежно появится try.
Синтаксически throws пишется после списка параметров и перед стрелкой -> (если она есть). Если функция ничего не возвращает (Void), throws всё равно остаётся на своём месте — это именно про «может упасть с причиной», а не про тип результата.
Небольшая шпаргалка по чтению сигнатуры:
| Сигнатура | Как читать по-человечески |
|---|---|
|
«Всегда вернёт Int» |
|
«Либо вернёт Int, либо бросит ошибку» |
|
«Либо выполнится, либо бросит ошибку» |
Мини‑пример: описываем ошибки ввода
Будем продолжать одно и то же мини‑приложение (условно назовём файл LibraryMini.swift): храним «книги» в памяти и постепенно учимся обрабатывать плохой ввод управляемо, а не «пусть всё упадёт». Сегодня нам достаточно ошибок ввода.
Пример (добавьте в LibraryMini.swift):
import Foundation
enum InputError: Error {
case empty(field: String)
case notANumber(field: String, input: String)
}
Здесь важно, что это не «сообщения», а типы причин. Мы ещё не ловим эти ошибки (это следующая лекция), но уже можем их создавать и бросать.
3. throw: досрочный выход из функции
В мире throws ключевая команда — throw. Она делает очень простую вещь: немедленно завершает текущую функцию и отдаёт наружу значение ошибки. Это не «вернуть ошибку» как return, и это не «распечатать и пойти дальше». Это прямой выход: вы «вылетаете» из функции сразу.
Поэтому логика в throwing‑функции часто выглядит максимально линейно: «проверка → иначе throw → дальше следующая проверка → иначе throw → и только потом нормальный return». Такой стиль особенно дружит с guard, потому что guard буквально создан для «линейного коридора правил».
import Foundation
enum InputError: Error { case empty(field: String) }
func requireNonEmpty(_ text: String, field: String) throws -> String {
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw InputError.empty(field: field)
}
return text
}
Обратите внимание на ощущение от чтения: сначала правило, затем причина отказа, затем «всё хорошо — возвращаем». Код после throw в этой ветке не выполнится никогда, и это сильно снижает шанс «продолжили работать в неправильном состоянии».
4. try: явная отметка места, где может быть ошибка
try — это не «поймать ошибку». try — это честная табличка «Осторожно» там, где можно поскользнуться. То есть try маркирует место, где выполнение может перейти на траекторию ошибки.
Swift заставляет писать try, потому что иначе код с ошибками становится похож на фильм ужасов: вроде идёте по коридору, а где именно выскочит монстр — неизвестно. С try видно всё: вот тут риск. Именно поэтому похожая идея применяется и к await: потенциальная «пауза» тоже должна быть явно отмечена в коде, чтобы вы видели точки, где управление может «съехать в сторону».
Если забыть try
Пример ошибки компиляции — так делать нельзя:
import Foundation
func demo() throws {
let title = requireNonEmpty("Swift", field: "title") // ошибка: missing 'try'
print(title)
}
Компилятор не «придирается». Он защищает вас от ситуации, когда вы вызываете опасную функцию так, будто она безопасная.
Правильный вызов с try
import Foundation
enum InputError: Error { case empty(field: String) }
func requireNonEmpty(_ text: String, field: String) throws -> String {
guard !text.isEmpty else { throw InputError.empty(field: field) }
return text
}
func demo() throws {
let title = try requireNonEmpty("Swift", field: "title")
print(title) // Swift
}
Тут мы вызываем requireNonEmpty через try, но ошибку пока не обрабатываем. Мы просто честно говорим: «в этом месте может прилететь ошибка». А что делать с ней — решим в следующей лекции через do/catch.
5. try внутри → часто throws снаружи: пробрасываем ошибку
Когда функция вызывает throwing‑функцию, у неё появляется выбор: либо обработать ошибку (позже через do/catch), либо пробросить её дальше. Проброс — это не лень и не «пусть кто-то другой разбирается». Это нормальная архитектурная мысль: «я на этом уровне не знаю, что делать, у меня нет контекста принятия решения».
Технически проброс выглядит так: функция становится throws, а внутри неё появляется try. То есть вы как бы говорите: «если там ошибка — я тоже завершаюсь ошибкой, не прячу её».
Мини‑пример: парсим год книги
Добавим ещё одну ошибку и функцию, которая превращает String в Int с понятной причиной отказа.
import Foundation
enum InputError: Error {
case notANumber(field: String, input: String)
}
func parseInt(_ text: String, field: String) throws -> Int {
guard let value = Int(text) else {
throw InputError.notANumber(field: field, input: text)
}
return value
}
Теперь соберём цепочку: «строка → число → проверка диапазона». Диапазон (например, год издания) — это уже не «формат», а «правило».
import Foundation
enum InputError: Error {
case outOfRange(field: String, value: Int, min: Int, max: Int)
}
func validateYear(_ year: Int) throws -> Int {
let min = 1450
let max = 2100
guard (min...max).contains(year) else {
throw InputError.outOfRange(field: "year", value: year, min: min, max: max)
}
return year
}
А теперь — «склейка» в одну функцию, которая принимает текстовые поля (как будто из ввода) и возвращает готовые данные для книги. Обратите внимание: здесь два try, потому что два потенциально «опасных» шага.
import Foundation
struct Book {
let title: String
let year: Int
}
func makeBook(titleText: String, yearText: String) throws -> Book {
let title = try requireNonEmpty(titleText, field: "title")
let year = try validateYear(try parseInt(yearText, field: "year"))
return Book(title: title, year: year)
}
Да, вложенный try validateYear(try parseInt(...)) выглядит чуть «плотно». Мы позже научимся делать это читабельнее (например, через промежуточную переменную), но сейчас нам важно увидеть механику: каждое место, где может быть ошибка, помечено try.
Для ясности — та же логика как схема:
flowchart TD
A[makeBook] --> B[try requireNonEmpty]
B --> C[try parseInt]
C --> D[try validateYear]
D --> E[return Book]
B -->|throw| X[ошибка наружу]
C -->|throw| X
D -->|throw| X
6. Как читать код с throws и try
Когда вы видите throws в сигнатуре, полезно мысленно произнести: «эта функция может закончиться двумя способами». А когда вы видите try, полезно сделать микропаузу и спросить себя: «а кто будет решать, что делать при ошибке?». Если ответ «не я», значит текущая функция, вероятно, тоже throws и просто пробрасывает.
Иногда помогает очень практичная табличка «что должно быть вокруг» (это не закон вселенной, но хорошая подсказка для новичков):
| Внутри функции есть… | Значит снаружи нужно… | Почему |
|---|---|---|
|
|
иначе throw запрещён |
|
или |
иначе непонятно, кто отвечает за ошибку |
|
или |
— такой же контракт |
Кстати, в стандартных API Swift вы это увидите постоянно. Например, в Codable методы encode(to:) и init(from:) тоже throwing: там много причин «не получилось», и их нельзя выразить одним nil.
Throwing‑инициализаторы: синтаксис и идея
Иногда вы хотите, чтобы сама сущность гарантировала корректность: «если объект создан — он валиден». Для этого в Swift есть throwing‑инициализатор: init(...) throws. По сути, это та же throwing‑функция, только в форме создания значения.
Сегодня мы не будем на нём долго жить (у нас фокус на throws + try именно в функциях), но важно хотя бы узнать синтаксис и не пугаться его в чужом коде.
import Foundation
struct Year {
let value: Int
init(_ value: Int) throws {
guard (1450...2100).contains(value) else {
throw InputError.outOfRange(field: "year", value: value, min: 1450, max: 2100)
}
self.value = value
}
}
С точки зрения вызывающего кода всё честно: раз init throwing, значит создание тоже требует try: let y = try Year(2020).
7. Типичные ошибки при работе с throws и try
Ошибка №1: путать try с обработкой ошибки.
Очень частая путаница: кажется, что раз написали try, то «ошибка как-то обработалась». Нет: try лишь разрешает компиляцию и отмечает риск. Если вы не написали обработку (через do/catch) и не пробросили выше (throws), то код либо не скомпилируется, либо будет архитектурно «в воздухе».
Ошибка №2: забыть добавить throws в сигнатуру, но написать throw внутри.
Это выглядит так, будто вы начали писать «умный код», а компилятор внезапно обиделся. Он не обиделся — он напоминает: выберите контракт. Либо функция всегда возвращает результат, либо честно заявляет, что может завершиться ошибкой. Если внутри есть throw, значит снаружи должен быть throws.
Ошибка №3: пытаться «спрятать» ошибку, возвращая фейковое значение.
Иногда новичок думает: «ну если год не распарсился — верну 0, и всё». Это очень опасно: дальше программа будет работать с мусором и выдавать странные результаты. Если ситуация действительно ошибочная, лучше бросить типизированную ошибку и дать верхнему уровню решить, как реагировать.
Ошибка №4: ставить try не там, где нужно.
try относится к конкретному выражению, которое может бросить ошибку. Если у вас в строке два вызова, и оба throwing, то try может понадобиться для каждого. Хороший симптом: если компилятор говорит missing 'try', не пытайтесь спорить — лучше разберите выражение на пару строк с промежуточными переменными, и сразу станет понятно, где именно риск.
Ошибка №5: делать одну гигантскую throwing‑функцию, которая валидирует всё подряд.
Такой код быстро превращается в «божественную функцию», которую страшно трогать. Гораздо приятнее (и вам, и будущему вам) держать валидацию маленькими шагами: requireNonEmpty, parseInt, validateYear. Тогда try в коде будет не «шумом», а маркерами понятных этапов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ