Практика 14. Практика к лекции 12 (часть 2)
Цель: понять механизм автоматического подсчёта ссылок (ARC), научиться обнаруживать и устранять циклические ссылки с помощью weak и unowned, корректно использовать capture lists в замыканиях и проектировать архитектуру без утечек памяти.
Рекомендации по выполнению:
- Создавайте отдельный
.swift-файл для каждого задания. - Компилируйте и запускайте через
swift <файл>.swift(Linux, без Xcode). - Для проверки освобождения объектов используйте
deinit { print("...") }. - Помните: ARC применяется только к классам (reference types), но не к структурам и перечислениям.
A. Разминка — ARC и deinit
- Создайте класс
Personсlet name: Stringиdeinit, печатающим"\(name) освобождён". В функцииmain():- создайте экземпляр в локальной области видимости (блок
do { ... }); - убедитесь, что
deinitвызывается при выходе из блока; - создайте две переменные, ссылающиеся на один экземпляр, обнулите одну — убедитесь, что
deinitне вызывается, пока жива вторая ссылка.
- создайте экземпляр в локальной области видимости (блок
Пример ожидаемого поведения:
class Person { let name: String init(name: String) { self.name = name } deinit { print("\(name) освобождён") }}
do { let p = Person(name: "Алиса") print("Внутри блока")}// Внутри блока// Алиса освобождён- Создайте класс
Nodeсvar value: Intиvar next: Node?. Постройте цепочку из трёх узлов. Обнулите головной узел и проверьте черезdeinit, что все три узла освобождаются (нет цикла).
B. Retain cycle — обнаружение и устранение
-
Создайте классы
AuthorиBookс взаимными сильными ссылками:Author:let name: String,var books: [Book];Book:let title: String,var author: Author?.
Продемонстрируйте, что после обнуления переменных
deinitне вызывается (утечка). Затем исправьте: сделайтеauthorвBookслабой ссылкой (weak var author: Author?) и покажите, что объекты корректно освобождаются.
Пример ожидаемого поведения (после исправления):
do { let author = Author(name: "Толстой") let book = Book(title: "Война и мир") author.books.append(book) book.author = author}// Book 'Война и мир' deinit// Author 'Толстой' deinit-
Создайте классы
ApartmentиTenant:Apartment:let address: String,var tenant: Tenant?;Tenant:let name: String,unowned let apartment: Apartment.
Объясните, почему здесь уместен
unowned(жилец не может существовать без квартиры). Продемонстрируйте освобождение обоих объектов.
C. Capture lists в замыканиях
- Создайте класс
TaskRunnerсо свойствомonComplete: (() -> Void)?и методомrun():
class TaskRunner { let name: String var onComplete: (() -> Void)?
init(name: String) { self.name = name }
func run() { print("\(name): задача выполняется") onComplete?() }
deinit { print("\(name) освобождён") }}- Сначала назначьте замыкание, захватывающее
selfсильно:runner.onComplete = { print(self.name) }. Покажите, чтоdeinitне вызывается (утечка). - Затем исправьте через
[weak self]иguard let selfвнутри замыкания. Убедитесь, чтоdeinitвызывается.
- Создайте класс
Counterсvar count = 0и методомmakeIncrementer() -> () -> Int, возвращающим замыкание, увеличивающееcount. Продемонстрируйте две версии:- с
[self](strong capture) — объект живёт, пока живо замыкание; - с
[weak self]— объект может быть освобождён, замыкание возвращает0приnil.
- с
Пример ожидаемого поведения:
var counter: Counter? = Counter()let inc = counter!.makeIncrementer()print(inc()) // 1print(inc()) // 2counter = nil// С [weak self]: Counter deinitprint(inc()) // 0 (self == nil)D. Паттерн Delegate с weak
-
Реализуйте паттерн делегата:
- Протокол
DownloadDelegate: AnyObjectс методомdidFinish(data: String); - Класс
Downloaderсо свойствомweak var delegate: DownloadDelegate?и методомstart(), вызывающимdelegate?.didFinish(data:)после «загрузки»; - Класс
ViewController, реализующийDownloadDelegate.
Покажите, что при обнулении
ViewControllerссылкаdelegateавтоматически становитсяnil(нет retain cycle). Объясните, почемуdelegateобъявлен какweakи почему протокол наследуетAnyObject. - Протокол
Пример ожидаемого поведения:
var vc: ViewController? = ViewController()let downloader = Downloader()downloader.delegate = vc
downloader.start() // "Загрузка завершена: <данные>"vc = nildownloader.start() // ничего — делегат nil// ViewController deinit- Расширьте задание 7: добавьте протокол
ProgressDelegate: AnyObjectс методомdidUpdate(progress: Double). КлассDownloaderдолжен поддерживать оба делегата. Оба —weak.
E. Диагностика утечек
- Вам дан следующий код с утечкой памяти. Найдите и исправьте все проблемы (их может быть несколько):
class Engine { var car: Car? let horsepower: Int init(horsepower: Int) { self.horsepower = horsepower } deinit { print("Engine deinit") }}
class Car { var engine: Engine? var onStart: (() -> Void)? let model: String init(model: String) { self.model = model } deinit { print("Car \(model) deinit") }}
func testLeak() { let car = Car(model: "Tesla") let engine = Engine(horsepower: 300) car.engine = engine engine.car = car car.onStart = { print("\(car.model) запущена с \(engine.horsepower) л.с.") }}testLeak()// Ожидание: оба deinit вызваны// Реальность: утечка — ни один deinit не вызванПеречислите все retain cycles и исправьте каждый.
F. Мини-проект
- «Система уведомлений» — примените все изученные концепции:
- Протокол
Observer: AnyObjectс методомnotify(event: String); - Класс
EventBus:- хранит массив слабых ссылок на наблюдателей;
- метод
subscribe(_ observer: Observer)— добавляет наблюдателя; - метод
emit(event: String)— уведомляет всех живых наблюдателей; - автоматически очищает
nil-ссылки приemit.
- Классы
LoggerиAnalytics, реализующиеObserver; - Продемонстрируйте:
- подписку нескольких наблюдателей;
- корректное уведомление;
- обнуление одного наблюдателя и повторный
emit(ушедший не вызывается); deinitвызывается для освобождённых наблюдателей.
Подсказка: для хранения слабых ссылок в массиве используйте обёртку:
struct WeakRef<T: AnyObject> { weak var value: T?}Пример ожидаемого поведения:
let bus = EventBus()var logger: Logger? = Logger(name: "FileLogger")var analytics: Analytics? = Analytics(name: "GA")
bus.subscribe(logger!)bus.subscribe(analytics!)
bus.emit(event: "user_login")// FileLogger: user_login// GA: user_login
analytics = nil// Analytics 'GA' deinit
bus.emit(event: "page_view")// FileLogger: page_view// (GA не вызван — ссылка nil)G. Критерии оценивания
- Корректное понимание ARC,
deinit, подсчёта ссылок: 0–4 балла - Обнаружение и устранение retain cycles (
weak/unowned): 0–4 балла - Правильное использование capture lists в замыканиях: 0–4 балла
- Мини-проект — полнота, отсутствие утечек, чистота архитектуры: 0–4 балла
Максимум: 16 баллов. Бонус до +2 за дополнительные задания.
H. Дополнительно (по желанию)
- Реализуйте
WeakArray<T: AnyObject>— обёртку над массивом слабых ссылок с автоматической очисткойnilпри доступе. - Сравните
weakиunownedв таблице: когда использовать, что происходит при обращении к освобождённому объекту, требования к optional. - Напишите пример с
unowned(unsafe)и объясните, почему его не следует использовать в продакшене.