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

Лекция 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 → 1

defer шире, чем 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

АспектSwiftPython
Объявление ошибкиenum MyError: Errorclass MyError(Exception)
Выбрасываниеthrow MyError.caseraise MyError()
Пометка функцииfunc f() throwsнет (любая функция может бросить)
Обработкаdo { try f() } catch {}try: f() except:
Гарантированный кодdefer {}finally:
Преобразование в niltry?нет аналога
Принудительный вызов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: "Алиса") // счётчик = 1
var ref2: Person? = ref1 // счётчик = 2
var ref3: Person? = ref1 // счётчик = 3
ref1 = nil // счётчик = 2
ref2 = nil // счётчик = 1
ref3 = 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 → Employee
emp!.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 = emp
emp!.department = dept
dept = nil // Отдел освобождён, emp.department = nil
emp = 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

Критерийweakunowned
Тип свойства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, a
del a, b
gc.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. Вопросы для самопроверки

  1. Чем отличается try, try? и try!? В каких ситуациях уместен каждый?
  2. Почему defer считается более гибким, чем finally в Python?
  3. Что произойдёт, если в функции несколько defer-блоков? В каком порядке они выполнятся?
  4. Чем throws отличается от rethrows?
  5. Когда Result предпочтительнее throws?
  6. Что такое ARC и к каким типам он применяется?
  7. Почему циклические ссылки приводят к утечке памяти при ARC?
  8. Чем weak отличается от unowned? Когда использовать каждый?
  9. Как избежать retain cycle в замыканиях? Что такое capture list?
  10. Чем ARC в Swift отличается от сборщика мусора в Python?