JavaRush /Курсы /Swift SELF /throws‑функции и вызов через try

throws‑функции и вызов через try

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

1. Две траектории выполнения: значение или ошибка

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

Если раньше мы часто делали так: «не получилось → вернём nil», то теперь добавляется другой контракт: «не получилось → вернём причину через throw». Причина не теряется, она не превращается в магический nil, и её можно обработать на уровне, где есть смысл принять решение.

Наглядно это можно представить так:

flowchart TD
    A[Вызов функции] --> B{Внутри всё ок?}
    B -->|Да| C[return результат]
    B -->|Нет| D[throw ошибка]

2. throws в сигнатуре: объявляем «опасную» функцию

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

Синтаксически throws пишется после списка параметров и перед стрелкой -> (если она есть). Если функция ничего не возвращает (Void), throws всё равно остаётся на своём месте — это именно про «может упасть с причиной», а не про тип результата.

Небольшая шпаргалка по чтению сигнатуры:

Сигнатура Как читать по-человечески
func f() -> Int
«Всегда вернёт Int»
func f() throws -> Int
«Либо вернёт Int, либо бросит ошибку»
func f() throws
«Либо выполнится, либо бросит ошибку»

Мини‑пример: описываем ошибки ввода

Будем продолжать одно и то же мини‑приложение (условно назовём файл 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 SomeError...
throws
иначе throw запрещён
try someThrowingCall()
do/catch
или
throws
иначе непонятно, кто отвечает за ошибку
try Type(...)
do/catch
или
throws
init throws
— такой же контракт

Кстати, в стандартных 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 в коде будет не «шумом», а маркерами понятных этапов.

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