$ sudo teach IT

Кто убирает за объектами

Каждый раз, когда вы создаёте объект класса, ему выделяется место в памяти. Рано или поздно это место нужно освободить — иначе программа будет постепенно съедать всю доступную память, пока не упадёт. Вопрос в том, кто следит за тем, что объект больше никому не нужен, и в какой момент его можно убрать.

В одних языках за это отвечает отдельный процесс, который время от времени останавливает программу и ищет мусор среди объектов. В Swift выбран другой путь: компилятор сам считает, сколько мест в коде ссылается на объект, и освобождает его ровно тогда, когда счётчик доходит до нуля. Этот механизм называется ARC — Automatic Reference Counting, автоматический подсчёт ссылок.

Сильные ссылки и счётчик

Когда вы присваиваете экземпляр класса переменной, константе или свойству, вы создаёте сильную ссылку (strong reference) на этот объект. У каждого объекта класса есть невидимый счётчик — сколько сильных ссылок на него сейчас существует. Пока счётчик больше нуля, объект живёт в памяти. Как только последняя сильная ссылка исчезает — переменная выходит из области видимости, свойство перезаписывается другим значением, — счётчик становится равен нулю, и объект немедленно освобождается.

Важно: несколько переменных могут указывать на один и тот же объект. Присваивание let second = first не создаёт копию объекта — оно создаёт вторую сильную ссылку на тот же самый объект, и счётчик увеличивается на единицу.

class Session {
    let id: Int
    init(id: Int) { self.id = id }
}

let first = Session(id: 1)
let second = first
// счётчик ссылок у объекта Session сейчас равен 2

Тут first и second — это две ссылки на один объект, а не два объекта. Изменение через одну ссылку будет видно и через другую, потому что это буквально одна и та же вещь в памяти.

deinit как наблюдаемый момент освобождения

У класса можно объявить deinit — специальный блок без параметров, который Swift вызывает сам, в тот самый момент, когда счётчик ссылок на объект доходит до нуля. Именно через deinit удобно увидеть работу ARC: если внутри него напечатать сообщение, вы буквально услышите момент освобождения объекта.

class Resource {
    let name: String

    init(name: String) {
        self.name = name
        print("Открыт ресурс: \(name)")
    }

    deinit {
        print("Ресурс закрыт: \(name)")
    }
}

print("Программа началась")
do {
    let resource = Resource(name: "файл.txt")
    print("Работаем с ресурсом: \(resource.name)")
}
print("Область видимости завершилась")

Блок do { ... } здесь используется не для обработки ошибок, а просто как область видимости: всё, что объявлено внутри фигурных скобок, живёт только до закрывающей скобки. Переменная resource — единственная сильная ссылка на объект Resource, и как только выполнение доходит до конца блока do, эта ссылка исчезает, счётчик обнуляется, и Swift сразу вызывает deinit. Программа напечатает пять строк в таком порядке: сначала «Программа началась», потом «Открыт ресурс», потом «Работаем с ресурсом», потом «Ресурс закрыт» — и только после этого «Область видимости завершилась». Освобождение происходит не когда-нибудь потом, а именно в момент выхода из блока, до следующей строки кода.

Что делает компилятор, а что не приходится делать вам

В языках без ARC и без сборщика мусора программист сам вызывает что-то вроде «выделить память» и «освободить память», и забытый вызов освобождения — частая причина утечек. В Swift вы никогда не пишете подобные команды руками. Компилятор сам расставляет в скомпилированном коде операции увеличения и уменьшения счётчика ссылок — при каждом присваивании сильной ссылки, при каждом выходе переменной из области видимости, при каждом удалении элемента из массива, который хранил ссылку на объект. Вы просто пишете обычный код с переменными и областями видимости, а ARC синхронно, по ходу выполнения, поддерживает счётчик в точности каждого объекта.

Чем это отличается от сборщика мусора

Сборщик мусора (garbage collector), который используется во многих других языках, устроен иначе: он не считает ссылки на лету, а время от времени запускает отдельный проход по памяти, ищет объекты, до которых больше нет пути от «живых» переменных, и удаляет их все разом. Из-за этого момент освобождения непредсказуем — объект может провисеть в памяти ещё какое-то время после того, как стал не нужен, а сама проверка способна на мгновение притормозить программу.

ARC работает по-другому: решение о том, где в коде нужно проверить счётчик, компилятор принимает заранее, во время компиляции, а не гадает во время выполнения. Освобождение происходит детерминированно — сразу, в конкретной строке, где обнуляется последняя сильная ссылка, без отдельной паузы на «уборку». Это удобно предсказуемостью, но накладывает свою цену: если два объекта держат сильные ссылки друг на друга, их счётчики никогда не дойдут до нуля сами по себе — с такой ситуацией вы разберётесь отдельно, когда пойдёт речь про слабые ссылки.

Почему у структур такого вопроса нет

Всё это касается только классов. Структуры и перечисления — типы значения: при присваивании переменной, передаче в функцию или сохранении в массив они не делятся одним объектом с общим счётчиком, а копируются целиком. У копии нет понятия «последняя ссылка на общий объект», поэтому у структур нет ни ARC, ни deinit — они просто перестают существовать вместе с переменной, которая их хранила, без отдельного наблюдаемого события.

Частые ошибки

  • Считать, что let second = first создаёт новый объект класса. На самом деле это вторая сильная ссылка на тот же самый объект — счётчик увеличивается, а не появляется копия.
  • Думать, что объект освобождается «через какое-то время после того, как стал не нужен», как при сборке мусора. В Swift это происходит сразу и в конкретном месте — как только счётчик сильных ссылок доходит до нуля.
  • Пытаться вызвать deinit вручную или добавить ему параметры — Swift вызывает его сам и только сам, без параметров.
  • Ждать deinit у структуры — у типов значения его нет вообще, потому что нет разделяемого объекта со счётчиком ссылок.
  • Забывать, что объекты внутри коллекции (например, массива) тоже держат сильную ссылку: пока объект лежит в массиве, счётчик не обнулится, даже если переменная, через которую его создали, давно вышла из области видимости.

Резюме

  • ARC — автоматический подсчёт сильных ссылок на объект класса; компилятор сам вставляет в код операции учёта ссылок, без ручных вызовов.
  • Каждое присваивание экземпляра класса переменной или свойству создаёт ещё одну сильную ссылку на тот же объект, а не копию.
  • Объект освобождается сразу, как только счётчик сильных ссылок на него доходит до нуля — обычно в момент выхода последней ссылающейся переменной из области видимости.
  • deinit — блок у класса, который Swift вызывает автоматически в момент освобождения объекта; вручную его вызвать нельзя, параметров у него нет.
  • В отличие от сборщика мусора, ARC освобождает память детерминированно и сразу, а не отдельным проходом во время выполнения.
  • У структур и перечислений ARC и deinit не нужны: как типы значения, они копируются целиком, а не делят общий объект со счётчиком ссылок.

Проверьте себя

4 вопроса

Наблюдаем освобождение объекта

Напишите программу верхнего уровня, которая наглядно показывает работу ARC.

Объявите класс с любым названием и хранимым свойством-именем. В инициализаторе напечатайте сообщение о том, что объект создан (включив в сообщение значение свойства). В deinit напечатайте сообщение о том, что объект освобождён (тоже со значением свойства).

Затем в коде верхнего уровня: напечатайте любое сообщение о начале программы, откройте вложенную область видимости через do { ... }, создайте внутри нее экземпляр вашего класса и присвойте его константе, напечатайте сообщение о том, что объект используется, закройте область видимости закрывающей скобкой и напечатайте сообщение о том, что область видимости завершилась.

Программа должна успешно скомпилироваться и завершиться без ошибок — конкретный текст сообщений не проверяется, важно само поведение: сообщение об освобождении из deinit должно появиться после выхода из do-блока и до финального сообщения о завершении области видимости.