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.