strong, weak и unowned: что выбрать

Три модификатора отличаются двумя признаками: удерживает ли ссылка объект и что происходит, когда объект всё-таки умер.

strong удерживает объект. weak не удерживает и автоматически становится nil. unowned не удерживает и не обнуляется — обращение после смерти объекта роняет приложение.

«Чем weak отличается от unowned?»

Что на самом деле проверяют

Формальное отличие («weak опциональный, unowned нет») знают все. Интервьюер ждёт следующего слоя: понимаете ли вы, как weak обнуляется, почему это стоит дороже, и по какому критерию вы выбираете между ними в реальном коде. Это прямой прокси на то, будете ли вы ронять прод.

Развёрнутый ответ

По умолчанию все ссылки в Swift сильные. Сильная ссылка увеличивает счётчик и гарантирует, что объект жив, пока она существует.

weak не увеличивает счётчик. Чтобы такая ссылка могла безопасно обнулиться, рантайм заводит для объекта side table — отдельную структуру рядом с объектом, где хранится список слабых ссылок и отдельный счётчик. Когда сильный счётчик обнуляется, объект разрушается, но side table живёт, пока на неё смотрит хотя бы одна weak-ссылка, и все они читаются как nil. Это и называют zeroing weak reference. Отсюда и цена: доступ к weak — не просто чтение указателя, а поход через side table с атомарной проверкой, поэтому weak заметно дороже strong в горячем цикле.

unowned не увеличивает счётчик и side table не заводит — по сути это сырой указатель с формальной проверкой. Если объект уже умер, обращение падает. Есть и совсем сырой вариант unowned(unsafe), который не проверяет вообще ничего: это классический dangling pointer со всеми последствиями вплоть до чтения чужой памяти.

final class Node {
    let name: String
    init(name: String) { self.name = name }
    deinit { print("deinit \(name)") }
}

var strongRef: Node? = Node(name: "A")
weak var weakRef = strongRef
unowned var unownedRef = strongRef!

print(weakRef?.name ?? "nil")   // A
strongRef = nil                 // объект разрушен
print(weakRef?.name ?? "nil")   // nil — weak обнулилась сама
// print(unownedRef.name)       // крэш: объект мёртв, ссылка об этом не знает

Вывод:

A
deinit A
nil

Правило выбора

Критерий один и он про время жизни, а не про удобство синтаксиса.

  • weak — если ссылка может законно стать nil: delegate, ссылка на родителя в дереве, self в долгоживущем замыкании, ссылка на контроллер, который пользователь может закрыть.
  • unowned — если объект гарантированно переживёт ссылку: два объекта, созданных вместе, где один по конструкции не может умереть раньше другого.
final class Customer {
    let name: String
    var card: CreditCard?          // карты может не быть
    init(name: String) { self.name = name }
}

final class CreditCard {
    let number: String
    unowned let owner: Customer    // карта без владельца не существует
    init(number: String, owner: Customer) {
        self.number = number
        self.owner = owner
    }
}

Здесь unowned честен: CreditCard создаётся только из Customer и умирает вместе с ним. Но на собеседовании безопаснее говорить так: «по умолчанию беру weak, unowned — только когда могу доказать, что объект переживёт ссылку». Цена ошибки несимметрична: лишний weak даёт мёртвый опционал, лишний unowned даёт крэш у пользователя.

«Почему weak-переменная обязана быть var и Optional?»

Что на самом деле проверяют

Это вопрос-детектор: он отделяет тех, кто заучил weak var delegate: Delegate? как заклинание, от тех, кто понимает механику обнуления.

Развёрнутый ответ

Ответ выводится из механики за одну секунду. Weak-ссылка обнуляется в произвольный момент — тогда, когда умер объект. Значит:

  • Значение переменной меняется помимо вашей воли, а менять значение let нельзя. Отсюда var.
  • Новым значением становится nil, а nil может хранить только Optional. Отсюда знак вопроса.

Компилятор так и говорит: 'weak' variable should have optional type и 'weak' must be a mutable variable. Отдельное следствие: в замыкании [weak self] даёт self типа Self?, и его надо разворачивать.

protocol PlayerDelegate: AnyObject {          // class-bound обязательно
    func playerDidFinish(_ player: Player)
}

final class Player {
    weak var delegate: PlayerDelegate?        // var + Optional — иначе не скомпилируется

    func finish() {
        delegate?.playerDidFinish(self)
    }
}

Почему протокол должен быть AnyObject: weak имеет смысл только для ссылочных типов. Если протокол может быть реализован структурой, компилятор не сможет применить к нему счётчик ссылок и откажется помечать свойство weak. Плюс сама модель делегата подразумевает объект, а не копию.

Таблица-памятка

МодификаторУдерживаетОбнуляетсяКрэшитКогда применять
strongданетнетвладение по умолчанию
weakнетда, автоматическинетdelegate, родитель, self в долгоживущем замыкании
unownedнетнетда, при обращении к мёртвомуобъект гарантированно переживёт ссылку

Типичные ошибки кандидатов

  • Говорить «unowned быстрее, поэтому беру его» — экономия наносекунд против крэша в проде.
  • Считать, что unowned обнуляется как weak, просто «без опционала».
  • Объяснять требование var и Optional как «так принято», а не через момент обнуления.
  • Забывать : AnyObject у протокола делегата и не понимать сообщение компилятора.
  • Ставить weak на всё подряд, включая @IBOutlet-независимые сильные связи, и терять объекты сразу после создания.
  • Думать, что weak бесплатен: в цикле на миллион итераций разница в доступе видна.

Как ответить кратко

Обе ссылки не удерживают объект и не увеличивают счётчик. Разница в том, что происходит после смерти объекта: weak автоматически становится nil — рантайм ведёт side table со списком слабых ссылок и зануляет их, поэтому weak обязана быть var и Optional и стоит чуть дороже. Unowned не обнуляется, это по сути сырой указатель, и обращение к мёртвому объекту роняет приложение. Правило: weak — когда ссылка может законно стать nil, delegate или self в долгоживущем замыкании; unowned — только когда я могу доказать, что объект переживёт ссылку. По умолчанию беру weak.

Проверьте себя
1. Что происходит с unowned-ссылкой после разрушения объекта?
AОна становится nil, просто без опционального типа
BОна остаётся указывать на освобождённую память, и обращение к ней роняет приложение
CОна автоматически превращается в strong и продлевает жизнь объекта
DКомпилятор не даст собрать код, где такое возможно
2. Почему weak-свойство обязано быть var и Optional?
AПотому что ARC требует опционал для любых свойств классов
BПотому что weak-ссылки всегда инициализируются позже других свойств
CПотому что рантайм может в любой момент записать в неё nil, а менять let и хранить nil в неопционале нельзя
DЭто соглашение стиля, компилятор допускает и let
3. Почему протокол делегата помечают как AnyObject?
AЧтобы протокол мог реализовать и класс, и структура
BЧтобы включить у методов протокола динамическую диспетчеризацию через Objective-C
CЧтобы протокол попал в автодополнение Xcode
DЧтобы свойство делегата можно было пометить weak — счётчик ссылок есть только у ссылочных типов