Практика 13. Практика к лекции 12 (часть 1)
Цель: научиться проектировать собственные типы ошибок, корректно выбрасывать и обрабатывать их с помощью do-catch, использовать try?/try!, defer и тип Result для надёжной обработки исключительных ситуаций в Swift.
Рекомендации по выполнению:
- Создавайте отдельный
.swift-файл для каждого задания или группы заданий. - Компилируйте и запускайте через
swift <файл>.swiftилиswift build(Linux, без Xcode). - Для каждого
enumошибок добавляйте понятные описания черезLocalizedErrorилиCustomStringConvertible. - Проверяйте пограничные случаи: пустые массивы, нулевые значения, отрицательные суммы.
A. Разминка — протокол Error и throw
-
Создайте перечисление
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("Карта заблокирована")}- Создайте перечисление
ValidationError: Errorс кейсамиtooShort(minLength: Int),invalidCharacter(Character),empty. Напишите функциюvalidatePassword(_ password: String) throws, проверяющую пароль (минимум 8 символов, только буквы и цифры, не пустой). Выбрасывайте наиболее конкретную ошибку.
B. try?, try! и преобразование ошибок
- Напишите функцию
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]- Напишите функцию
parseInt(_ s: String) -> Result<Int, ParseError>, которая возвращает.successс числом или.failure(.notANumber(s)). Затем напишите обёрткуparseIntThrowing(_ s: String) throws -> Int, конвертирующуюResultвthrows. Покажите оба способа обработки.
C. defer и управление ресурсами
-
Напишите функцию
processFile(at path: String) throws -> String, которая:- печатает
"Открытие файла: \(path)"в начале; - использует
defer { print("Закрытие файла: \(path)") }; - если
pathсодержит"invalid", выбрасывает ошибкуFileError.notFound(path); - иначе возвращает
"Содержимое файла \(path)". Убедитесь, чтоdeferсрабатывает и при ошибке, и при успехе.
- печатает
-
Напишите функцию с тремя вложенными
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-1D. Result и rethrows
-
Создайте функцию
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 { ... }.
-
Напишите функцию
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 — нужен tryenum DoubleError: Error { case overflow }let riskyResult = try applyToAll([1, 2, 100]) { guard $0 < 50 else { throw DoubleError.overflow } return $0 * 2}E. Комплексная обработка ошибок
-
Реализуйте конвейер обработки данных:
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. Мини-проект
- «Банковская система» — объедините все навыки:
enum BankError: Error—insufficientFunds,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:)— напишите обе версии и опишите, когда каждый удобнее.