Swift: ссылочные типы и циклы сохранения, weak и unowned

Ответ на один из 10 технических вопросов недавнего собеседования по Swift и Objective C был очевиден, но нуждался в точном объяснении.

Вот этот вопрос:

«Какие ссылки на объект применяются в Swift для предотвращения цикла сохранения:

a) сильные; б) слабые; в) бесхозные?»

Оптимальный ответ: б) слабые.

Но почему?

Циклы сохранения

Если вкратце, для управления памятью в Swift используется автоматический подсчет ссылок.

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

«А как насчет значений-типов вроде структур?»

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

Копии этих значений создаются их передачей как аргументов или сохранением в других переменных. Например, при передаче в Swift в качестве аргумента функции значения-типа, такого как структура или перечисление, создается копия этого значения и таким образом выделяется новый кусок памяти. Это называется семантикой копирования.

Возьмем приложение с двумя классами:

class Manager: EmployeeDelegate {

let employee: Employee

init(employee: Employee) {
self.employee = employee
self.employee.delegate = self
}

func perform() {/* код */}
...
}

protocol EmployeeDelegate: AnyObject {
func perform()
}

class Employee {

public var delegate: EmployeeDelegate?

...
}

В Swift это типичный шаблон с классом, который «знает», когда выполнять операцию, но неизвестно какую. В этом случае шаблоном делегирования с protocol гарантируется, что руководитель Manager определяет операцию и дает указания работнику Employee, который в нужный момент их выполняет.

Сильные ссылки

Проблема в том, что в этом примере очень четко генерируется цикл сохранения. Почему? Когда создается экземпляр Manager, в него передается Employee, который сохраняется, и создается сильная ссылка, так что память от объекта Employee не освобождается, пока существует его Manager.

Чтобы определить операции JobDelegate, для Employee нужен делегат. Здесь в методе инициализатора в качестве делегата задан объект Manager, так что в Employee тоже имеется сильная ссылка на Manager и память от экземпляра Manager не освобождается, пока существует его Employee.

То есть они не дадут друг другу освободить память в течение неопределенного времени:

Слабые и бесхозные ссылки

Теперь главный вопрос: почему для предотвращения цикла сохранения применяется слабая weak ссылка, а не бесхозная unowned, ведь обе они нейтральны по своему влиянию на retainCount?

//Рассмотрим два класса:
class Manager: JobDelegate {
func operate() {
print("Operation executed.")
}
}

protocol JobDelegate: AnyObject { func operate() }

class Employee {

(unowned/weak) var delegate: JobDelegate?
//Этот модификатор будет единственной переменной для обоих запусков.

public func performOperation() {
delegate?.operate()
}
}

//ПРИМЕР
var employee = Employee()
var manager: Manager? = Manager() //выделяется память

employee.delegate = manager //в качестве делегата задается «Manager»

manager = nil //память освобождается

employee.performOperation()

unowned применяется, когда «сильное» владение в ссылке не требуется, но в большинстве сценариев unowned не рекомендуется. Поскольку он не может быть Optional, всегда предполагается, что в нем содержится объект, пока используется ссылка.

Для этого нужна уверенность в том, что объект, на который ссылаются, «переживет» ссылку или обоими память высвободится одновременно. Доступ к ссылке unowned после освобождения памяти чреват аварийным завершением работы:

/*
unowned var delegate: JobDelegate?

Обозначение здесь «delegate» как «Optional» никак не сказывается. Попытка получить доступ к «unowned» с «Optional», когда он освобожден, по-прежнему чревата аварийным завершением работы приложения.

Для использования «unowned» в этом случае потребовалось бы полное понимание жизненных циклов этих объектов и соответственных рисков.
*/

employee.performOperation() //Работа приложения аварийно завершилась.

/*
Лог:
ошибка: выполнение прервано, причина: сигнал «SIGABRT».
*/

С другой стороны, слабой ссылкой weak не только избегается «сильное» владение в ссылке, она всегда Optional и автоматически становится nil при высвобождении объекта.

Поэтому хороша для сценариев, где объект, на который ссылаются, выходит из области видимости или освобождается независимо от ссылки:

/*
weak var delegate: JobDelegate?

Здесь «delegate» в обязательном порядке помечается компилятором как «Optional».
*/

employee.performOperation() //Функция выполнена.

/*
Ничего не регистрируется.
*/

Заключение

Учитывая, что об отношении конкретного владения и жизненного цикла экземпляра на основе данных вопроса просто нечего сказать, в силу изложенных причин предпочтительным ссылочным типом становится weak.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Matheus Quirino Cardoso: Swift | Reference types and Retain cycles (Weak vs. Unowned) | Memory #1

Предыдущая статьяИскусство манипулирования массивами JavaScript: исследование метода Array.prototype.filter()
Следующая статьяSCDB: простая Open Source БД типа «ключ — значение»