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

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

Цель: понять механизм автоматического подсчёта ссылок (ARC), научиться обнаруживать и устранять циклические ссылки с помощью weak и unowned, корректно использовать capture lists в замыканиях и проектировать архитектуру без утечек памяти.

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

  • Создавайте отдельный .swift-файл для каждого задания.
  • Компилируйте и запускайте через swift <файл>.swift (Linux, без Xcode).
  • Для проверки освобождения объектов используйте deinit { print("...") }.
  • Помните: ARC применяется только к классам (reference types), но не к структурам и перечислениям.

A. Разминка — ARC и deinit

  1. Создайте класс 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("Внутри блока")
}
// Внутри блока
// Алиса освобождён
  1. Создайте класс Node с var value: Int и var next: Node?. Постройте цепочку из трёх узлов. Обнулите головной узел и проверьте через deinit, что все три узла освобождаются (нет цикла).

B. Retain cycle — обнаружение и устранение

  1. Создайте классы 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
  1. Создайте классы Apartment и Tenant:

    • Apartment: let address: String, var tenant: Tenant?;
    • Tenant: let name: String, unowned let apartment: Apartment.

    Объясните, почему здесь уместен unowned (жилец не может существовать без квартиры). Продемонстрируйте освобождение обоих объектов.


C. Capture lists в замыканиях

  1. Создайте класс 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 вызывается.
  1. Создайте класс Counter с var count = 0 и методом makeIncrementer() -> () -> Int, возвращающим замыкание, увеличивающее count. Продемонстрируйте две версии:
    • с [self] (strong capture) — объект живёт, пока живо замыкание;
    • с [weak self] — объект может быть освобождён, замыкание возвращает 0 при nil.

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

var counter: Counter? = Counter()
let inc = counter!.makeIncrementer()
print(inc()) // 1
print(inc()) // 2
counter = nil
// С [weak self]: Counter deinit
print(inc()) // 0 (self == nil)

D. Паттерн Delegate с weak

  1. Реализуйте паттерн делегата:

    • Протокол 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 = nil
downloader.start() // ничего — делегат nil
// ViewController deinit
  1. Расширьте задание 7: добавьте протокол ProgressDelegate: AnyObject с методом didUpdate(progress: Double). Класс Downloader должен поддерживать оба делегата. Оба — weak.

E. Диагностика утечек

  1. Вам дан следующий код с утечкой памяти. Найдите и исправьте все проблемы (их может быть несколько):
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. Мини-проект

  1. «Система уведомлений» — примените все изученные концепции:
  • Протокол 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) и объясните, почему его не следует использовать в продакшене.