Перейти к содержимому

Практика 13. Практика к лекции 12 (часть 1)

Цель: научиться проектировать собственные типы ошибок, корректно выбрасывать и обрабатывать их с помощью do-catch, использовать try?/try!, defer и тип Result для надёжной обработки исключительных ситуаций в Swift.

Рекомендации по выполнению:

  • Создавайте отдельный .swift-файл для каждого задания или группы заданий.
  • Компилируйте и запускайте через swift <файл>.swift или swift build (Linux, без Xcode).
  • Для каждого enum ошибок добавляйте понятные описания через LocalizedError или CustomStringConvertible.
  • Проверяйте пограничные случаи: пустые массивы, нулевые значения, отрицательные суммы.

A. Разминка — протокол Error и throw

  1. Создайте перечисление ATMError: Error с кейсами:

    • insufficientFunds(required: Double) — недостаточно средств;
    • invalidPIN — неверный PIN-код;
    • cardBlocked — карта заблокирована.

    Напишите функцию withdraw(amount: Double, pin: String, balance: Double) throws -> Double, которая:

    • выбрасывает cardBlocked, если pin == "0000";
    • выбрасывает invalidPIN, если pin != "1234";
    • выбрасывает insufficientFunds(required:), если amount > balance;
    • иначе возвращает новый баланс.

    Обработайте все ошибки в do-catch с осмысленными сообщениями.

Пример ожидаемого поведения:

do {
let newBalance = try withdraw(amount: 500, pin: "1234", balance: 1000)
print("Остаток: \(newBalance)") // Остаток: 500.0
} catch ATMError.insufficientFunds(let required) {
print("Не хватает средств, нужно: \(required)")
} catch ATMError.invalidPIN {
print("Неверный PIN")
} catch ATMError.cardBlocked {
print("Карта заблокирована")
}
  1. Создайте перечисление ValidationError: Error с кейсами tooShort(minLength: Int), invalidCharacter(Character), empty. Напишите функцию validatePassword(_ password: String) throws, проверяющую пароль (минимум 8 символов, только буквы и цифры, не пустой). Выбрасывайте наиболее конкретную ошибку.

B. try?, try! и преобразование ошибок

  1. Напишите функцию divideAll(_ numbers: [Double], by divisor: Double) throws -> [Double], выбрасывающую ошибку MathError.divisionByZero при делении на ноль. Затем:
    • вызовите её с try? и обработайте nil;
    • покажите случай, когда try! безопасен (делитель заведомо не ноль);
    • добавьте defer { print("divideAll завершена") } в начало функции и убедитесь, что сообщение печатается при любом исходе.

Пример ожидаемого поведения:

let result = try? divideAll([10, 20, 30], by: 0)
print(result as Any) // nil
let safe = try! divideAll([10, 20, 30], by: 5)
print(safe) // [2.0, 4.0, 6.0]
  1. Напишите функцию parseInt(_ s: String) -> Result<Int, ParseError>, которая возвращает .success с числом или .failure(.notANumber(s)). Затем напишите обёртку parseIntThrowing(_ s: String) throws -> Int, конвертирующую Result в throws. Покажите оба способа обработки.

C. defer и управление ресурсами

  1. Напишите функцию processFile(at path: String) throws -> String, которая:

    • печатает "Открытие файла: \(path)" в начале;
    • использует defer { print("Закрытие файла: \(path)") };
    • если path содержит "invalid", выбрасывает ошибку FileError.notFound(path);
    • иначе возвращает "Содержимое файла \(path)". Убедитесь, что defer срабатывает и при ошибке, и при успехе.
  2. Напишите функцию с тремя вложенными defer-блоками, печатающими "defer-1", "defer-2", "defer-3". Вызовите её и убедитесь, что порядок выполнения обратный (LIFO): defer-3, defer-2, defer-1.

Пример ожидаемого поведения:

func tripleDefer() {
defer { print("defer-1") }
defer { print("defer-2") }
defer { print("defer-3") }
print("Тело функции")
}
tripleDefer()
// Тело функции
// defer-3
// defer-2
// defer-1

D. Result и rethrows

  1. Создайте функцию fetchUser(id: Int) -> Result<String, NetworkError>, где NetworkError имеет кейсы notFound(id: Int), serverError(code: Int). Для id < 0 возвращайте .failure(.notFound(id:)), для id == 0.failure(.serverError(code: 500)), иначе — .success("User_\(id)"). Обработайте результат через:

    • switch по Result;
    • метод .map { ... };
    • метод .flatMap { ... }.
  2. Напишите функцию applyToAll<T>(_ items: [T], transform: (T) throws -> T) rethrows -> [T], применяющую transform к каждому элементу массива. Продемонстрируйте, что:

    • при передаче не выбрасывающего замыкания результат можно вызывать без try;
    • при передаче выбрасывающего замыкания требуется try.

Пример ожидаемого поведения:

// Без throws — try не нужен
let doubled = applyToAll([1, 2, 3]) { $0 * 2 }
print(doubled) // [2, 4, 6]
// С throws — нужен try
enum DoubleError: Error { case overflow }
let riskyResult = try applyToAll([1, 2, 100]) {
guard $0 < 50 else { throw DoubleError.overflow }
return $0 * 2
}

E. Комплексная обработка ошибок

  1. Реализуйте конвейер обработки данных:

    • func readInput() throws -> String — возвращает строку или выбрасывает PipelineError.noInput;
    • func parse(_ input: String) throws -> [Int] — парсит числа, разделённые запятыми, выбрасывает PipelineError.invalidFormat(input);
    • func compute(_ values: [Int]) throws -> Double — вычисляет среднее, выбрасывает PipelineError.emptyData при пустом массиве.

    Свяжите их в func pipeline() throws -> Double и обработайте каждую ошибку в do-catch на верхнем уровне. Используйте defer для логирования начала и конца конвейера.


F. Мини-проект

  1. «Банковская система» — объедините все навыки:
  • enum BankError: ErrorinsufficientFunds, accountNotFound(id: String), negativeAmount, transferToSelf.
  • struct Account с id: String, holder: String, private(set) var balance: Double.
  • struct Bank с массивом счетов и методами:
    • func findAccount(id: String) throws -> Account;
    • mutating func deposit(to id: String, amount: Double) throws;
    • mutating func withdraw(from id: String, amount: Double) throws;
    • mutating func transfer(from: String, to: String, amount: Double) throws.
  • Метод transfer должен использовать defer для логирования операции.
  • Все методы возвращают или выбрасывают, а не используют print внутри.
  • На верхнем уровне продемонстрируйте: успешный перевод, попытку перевода на тот же счёт, перевод суммы больше баланса, перевод на несуществующий счёт.

Пример ожидаемого поведения:

var bank = Bank(accounts: [
Account(id: "A1", holder: "Анна", balance: 1000),
Account(id: "A2", holder: "Борис", balance: 500),
])
do {
try bank.transfer(from: "A1", to: "A2", amount: 300)
print("Перевод выполнен")
} catch {
print("Ошибка: \(error)")
}
// Лог: Операция transfer A1 -> A2 завершена
// Перевод выполнен

G. Критерии оценивания

  • Корректность типов ошибок и полнота обработки: 0–5 баллов
  • Правильное использование do-catch, try?/try!, defer, Result, rethrows: 0–5 баллов
  • Качество кода: именование, отсутствие force unwrapping, читаемость: 0–3 балла
  • Мини-проект — полнота и продуманность: 0–3 балла

Максимум: 16 баллов. Бонус до +2 за дополнительные задания.


H. Дополнительно (по желанию)

  • Добавьте в BankError поддержку LocalizedError с понятными errorDescription.
  • Напишите функцию retry<T>(times: Int, task: () throws -> T) rethrows -> T, повторяющую task до times раз при ошибке.
  • Сравните подходы throws и Result для fetchUser(id:) — напишите обе версии и опишите, когда каждый удобнее.