Лекция 12. Обработка ошибок и управление памятью
1. Введение
Любая реальная программа сталкивается с ситуациями, когда что-то идёт не так: файл не найден, сеть недоступна, пользователь ввёл некорректные данные. Swift предлагает строгую модель обработки ошибок, заставляющую разработчика явно учитывать возможные сбои. Кроме того, Swift использует автоматический подсчёт ссылок (ARC) для управления памятью — механизм, принципиально отличающийся от сборщика мусора в Python.
Часть 1: Обработка ошибок
2. Модель обработки ошибок
Ошибки представляются значениями типов, соответствующих протоколу Error. Функция, которая может завершиться с ошибкой, помечается throws. Вызывающий код обязан обработать ошибку — компилятор не позволит её проигнорировать.
3. Определение типов ошибок
Ошибки определяются как enum, реализующие протокол Error:
enum FileError: Error { case notFound(filename: String) case permissionDenied case corruptedData}
enum NetworkError: Error { case noConnection case timeout(seconds: Int) case invalidURL(url: String)}Каждый кейс может хранить связанные значения с информацией об ошибке. Сравнение с Python:
class FileNotFoundError(Exception): def __init__(self, filename: str): self.filename = filename super().__init__(f"Файл не найден: {filename}")4. Выбрасывание ошибок и пометка функций
Оператор throw выбрасывает ошибку. Функция помечается throws перед стрелкой возвращаемого типа:
func readFile(named filename: String) throws -> String { if filename.isEmpty { throw FileError.notFound(filename: "(пустое имя)") } if filename == "secret.txt" { throw FileError.permissionDenied } return "Содержимое файла \(filename)"}5. Обработка ошибок: do-catch
Для вызова throwing-функции используется do-catch с ключевым словом try:
do { let content = try readFile(named: "data.txt") print(content)} catch FileError.notFound(let filename) { print("Ошибка: файл '\(filename)' не найден")} catch FileError.permissionDenied { print("Ошибка: нет прав доступа")} catch { print("Неизвестная ошибка: \(error)")}tryобязателен перед каждым вызовом throwing-функцииcatch-блоки поддерживают pattern matching для конкретных случаев- Последний
catchбез шаблона ловит всё; переменнаяerrorсодержит ошибку
6. Множественные catch-блоки и pattern matching
enum ValidationError: Error { case tooShort(minimum: Int) case tooLong(maximum: Int) case invalidCharacter(Character)}
func validate(password: String) throws { if password.count < 8 { throw ValidationError.tooShort(minimum: 8) } if password.count > 64 { throw ValidationError.tooLong(maximum: 64) } for char in password { if char == " " { throw ValidationError.invalidCharacter(char) } }}
do { try validate(password: "abc")} catch ValidationError.tooShort(let min) { print("Пароль слишком короткий, минимум \(min) символов")} catch ValidationError.tooLong(let max) { print("Пароль слишком длинный, максимум \(max) символов")} catch ValidationError.invalidCharacter(let char) { print("Недопустимый символ: '\(char)'")} catch { print("Ошибка: \(error)")}7. try? и try!
try? преобразует результат в Optional — при ошибке возвращается nil:
if let content = try? readFile(named: "data.txt") { print("Прочитано: \(content)")} else { print("Не удалось прочитать файл")}try! принудительно извлекает результат. При ошибке — аварийное завершение:
// Используйте ТОЛЬКО когда на 100% уверены, что ошибка невозможнаlet content = try! readFile(named: "config.txt")try! аналогичен force unwrap (!) — применяйте крайне осторожно.
8. Функции, пробрасывающие ошибки: rethrows
rethrows помечает функцию, которая выбрасывает ошибку только если переданное замыкание выбрасывает:
func perform(times: Int, action: () throws -> Void) rethrows { for _ in 0..<times { try action() }}
// С обычным замыканием — try не нужен:perform(times: 3) { print("Привет!") }
// С throwing замыканием — try обязателен:do { try perform(times: 2) { try validate(password: "ab") }} catch { print("Ошибка: \(error)")}Стандартная библиотека использует rethrows — например, map на массивах.
9. defer — гарантированное выполнение при выходе из скоупа
defer выполняется при выходе из области видимости, независимо от причины — return, throw или break:
func processFile(named filename: String) throws { print("Открываем файл: \(filename)") defer { print("Закрываем файл: \(filename)") } let content = try readFile(named: filename) print("Обрабатываем: \(content)")}Несколько defer-блоков выполняются в обратном порядке (LIFO):
func example() { defer { print("1") } defer { print("2") } defer { print("3") } print("Тело функции")}example()// Вывод: Тело функции → 3 → 2 → 1defer шире, чем finally в Python — он привязан к любому скоупу, а не только к try.
10. Result<Success, Failure> — альтернативный подход
Тип Result представляет результат операции без throws:
enum DatabaseError: Error { case connectionFailed case queryFailed(String)}
func fetchUser(id: Int) -> Result<String, DatabaseError> { if id <= 0 { return .failure(.queryFailed("Некорректный ID")) } return .success("Пользователь #\(id)")}
switch fetchUser(id: 42) {case .success(let user): print("Найден: \(user)")case .failure(let error): print("Ошибка: \(error)")}Result также имеет метод get(), который является throwing-функцией:
let user = try? fetchUser(id: 42).get()11. Сравнение обработки ошибок: Swift vs Python
| Аспект | Swift | Python |
|---|---|---|
| Объявление ошибки | enum MyError: Error | class MyError(Exception) |
| Выбрасывание | throw MyError.case | raise MyError() |
| Пометка функции | func f() throws | нет (любая функция может бросить) |
| Обработка | do { try f() } catch {} | try: f() except: |
| Гарантированный код | defer {} | finally: |
| Преобразование в nil | try? | нет аналога |
| Принудительный вызов | try! | нет аналога |
| Результат как значение | Result<S, F> | нет стандартного аналога |
Главное отличие: в Swift компилятор контролирует обработку ошибок. В Python исключения полностью динамические.
Часть 2: Управление памятью
12. Автоматический подсчёт ссылок (ARC)
Swift использует ARC (Automatic Reference Counting) для управления памятью экземпляров классов. ARC отслеживает количество сильных ссылок на объект и освобождает память, когда счётчик достигает нуля.
Важно: ARC применяется только к ссылочным типам (классам). Структуры и перечисления — типы значений, управляются через стек.
13. Как работает ARC: пример
class Person { let name: String init(name: String) { self.name = name print("\(name): инициализирован") } deinit { print("\(name): освобождён из памяти") }}
var ref1: Person? = Person(name: "Алиса") // счётчик = 1var ref2: Person? = ref1 // счётчик = 2var ref3: Person? = ref1 // счётчик = 3
ref1 = nil // счётчик = 2ref2 = nil // счётчик = 1ref3 = nil // счётчик = 0 → deinit → память освобождена14. Проблема: циклические сильные ссылки (retain cycle)
Когда два объекта ссылаются друг на друга, их счётчики никогда не обнулятся — утечка памяти:
class Department { let name: String var head: Employee? init(name: String) { self.name = name } deinit { print("Отдел '\(name)' освобождён") }}
class Employee { let name: String var department: Department? // сильная ссылка! init(name: String) { self.name = name } deinit { print("Сотрудник '\(name)' освобождён") }}
var dept: Department? = Department(name: "IT")var emp: Employee? = Employee(name: "Иван")
dept!.head = emp // Department → Employeeemp!.department = dept // Employee → Department
dept = nil // счётчик Department = 1 (ссылка от Employee)emp = nil // счётчик Employee = 1 (ссылка от Department)// deinit НЕ вызывается — утечка!15. Решение: weak — слабые ссылки
weak не увеличивает счётчик. При освобождении объекта ссылка автоматически становится nil. Поэтому weak-свойства всегда опциональные (var):
class Employee { let name: String weak var department: Department? // слабая ссылка init(name: String) { self.name = name } deinit { print("Сотрудник '\(name)' освобождён") }}
var dept: Department? = Department(name: "IT")var emp: Employee? = Employee(name: "Иван")dept!.head = empemp!.department = dept
dept = nil // Отдел освобождён, emp.department = nilemp = nil // Сотрудник освобождён ✓16. Решение: unowned — бесхозные ссылки
unowned также не увеличивает счётчик, но не обнуляется. Обращение после освобождения — crash:
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("Клиент '\(name)' освобождён") }}
class CreditCard { let number: String unowned let owner: Customer // бесхозная ссылка init(number: String, owner: Customer) { self.number = number self.owner = owner } deinit { print("Карта \(number) освобождена") }}
var customer: Customer? = Customer(name: "Мария")customer!.card = CreditCard(number: "1234-5678", owner: customer!)customer = nil // Оба объекта корректно освобождены ✓Карта не может существовать без владельца — идеальный случай для unowned.
17. Когда weak, когда unowned
| Критерий | weak | unowned |
|---|---|---|
| Тип свойства | Optional (var) | Может быть let, не-optional |
| При освобождении | Становится nil | Не обнуляется (crash) |
| Когда использовать | Объект может быть освобождён раньше | Объект гарантированно живёт дольше |
| Безопасность | Безопаснее | Быстрее, но опаснее |
Правило: если сомневаетесь — используйте weak.
18. Циклические ссылки в замыканиях: capture list
Замыкания захватывают объекты по сильной ссылке. Если объект хранит замыкание, обращающееся к self — возникает цикл:
class Timer { let interval: Int var onTick: (() -> Void)?
init(interval: Int) { self.interval = interval }
func start() { onTick = { print("Тик каждые \(self.interval) сек") // retain cycle! } } deinit { print("Timer освобождён") }}
var timer: Timer? = Timer(interval: 5)timer!.start()timer = nil // deinit НЕ вызывается — утечка!Решение — capture list с [weak self]:
func start() { onTick = { [weak self] in guard let self = self else { return } print("Тик каждые \(self.interval) сек") }}С [unowned self] — компактнее, но опаснее (crash, если объект уже освобождён):
onTick = { [unowned self] in print("Тик каждые \(self.interval) сек")}19. Сравнение управления памятью: Swift vs Python
| Аспект | Swift (ARC) | Python (GC + RC) |
|---|---|---|
| Механизм | Подсчёт ссылок (компиляция) | Подсчёт ссылок + сборщик мусора |
| Освобождение | Сразу при обнулении счётчика | RC — сразу; циклы — при сборке GC |
| Циклы | Ответственность программиста | Автоматически (модуль gc) |
| Предсказуемость | Высокая | Ниже (GC недетерминированный) |
| Деинициализатор | deinit — гарантированно | __del__ — может не вызваться |
import gc
class Node: def __init__(self): self.ref = None
a, b = Node(), Node()a.ref, b.ref = b, adel a, bgc.collect() # Python обнаружит и соберёт цикл автоматическиВ Swift аналогичный код привёл бы к утечке без weak/unowned.
20. Упражнения
Упражнение 1. Создайте enum ATMError: Error с кейсами insufficientFunds(required: Double), invalidPIN, cardBlocked. Напишите функцию withdraw(amount:pin:) throws, обработайте все ошибки в do-catch.
Упражнение 2. Напишите функцию divideAll(_ numbers: [Double], by divisor: Double) throws -> [Double], выбрасывающую ошибку при делении на ноль. Используйте try? и defer для логирования.
Упражнение 3. Создайте классы Author и Book с взаимными ссылками. Избегите retain cycle с помощью weak или unowned. Проверьте через deinit, что объекты освобождаются.
Упражнение 4. Создайте класс TaskRunner со свойством onComplete: (() -> Void)?. Продемонстрируйте утечку из-за захвата self в замыкании и исправьте её через [weak self].
Упражнение 5. Перепишите fetchUser(id:) из раздела 10, используя throws вместо Result. Сравните удобство обоих подходов.
21. Вопросы для самопроверки
- Чем отличается
try,try?иtry!? В каких ситуациях уместен каждый? - Почему
deferсчитается более гибким, чемfinallyв Python? - Что произойдёт, если в функции несколько
defer-блоков? В каком порядке они выполнятся? - Чем
throwsотличается отrethrows? - Когда
Resultпредпочтительнееthrows? - Что такое ARC и к каким типам он применяется?
- Почему циклические ссылки приводят к утечке памяти при ARC?
- Чем
weakотличается отunowned? Когда использовать каждый? - Как избежать retain cycle в замыканиях? Что такое capture list?
- Чем ARC в Swift отличается от сборщика мусора в Python?