Почему лучше не использовать Unowned?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему избегать Unowned в современном Swift
Unowned ссылка казалась хорошей идеей, но на практике она опасна и может привести к крашам. Вот почему современные разработчики предпочитают weak.
Что такое Unowned
Unowned ссылка похожа на weak, но:
- НЕ опциональна
- НЕ становится nil при удалении объекта
- Может вызвать CRASH если объект удалён
class CreditCard {
let owner: Person // Требует Person
unowned let parentCard: CreditCard? // Ошибка: не может быть опциональным!
init(owner: Person) {
self.owner = owner
}
}
Главная проблема: CRASH при dangling reference
Если объект удалён, а unowned ссылка на него используется — приложение КРАШИТСЯ.
class Person {
let name: String
init(name: String) { self.name = name }
deinit { print("Person deinitialized") }
}
class Pet {
unowned let owner: Person
init(owner: Person) {
self.owner = owner
}
func printOwnerName() {
print(owner.name) // CRASH если owner был удалён!
}
}
var pet: Pet?
do {
let person = Person(name: "Alice")
pet = Pet(owner: person)
} // person удалён отсюда
pet?.printOwnerName() // CRASH: Thread 1: EXC_BAD_ACCESS
Vs. Weak (безопасно):
class Pet {
weak var owner: Person? // Опциональна, не крашится
init(owner: Person) {
self.owner = owner
}
func printOwnerName() {
if let owner = owner {
print(owner.name)
} else {
print("Owner was deallocated")
}
}
}
var pet: Pet?
do {
let person = Person(name: "Alice")
pet = Pet(owner: person)
}
pet?.printOwnerName() // Печатает "Owner was deallocated", без крашей
Проблема 1: Непредсказуемость в asyncly-concurrent коде
В современном Swift с async/await, объекты могут удалиться в неожиданный момент.
class DataManager {
unowned let delegate: UIViewController
init(delegate: UIViewController) {
self.delegate = delegate
}
func fetchData() async {
let data = try await apiService.fetch()
// К этому моменту UIViewController мог быть popped с stack'а!
self.delegate.displayData(data) // CRASH
}
}
Безопасно с weak:
class DataManager {
weak var delegate: UIViewController?
func fetchData() async {
let data = try await apiService.fetch()
guard let delegate = delegate else { return }
delegate.displayData(data)
}
}
Проблема 2: Трудно понять lifecycle
У unowned нет явного механизма отслеживания. Нужно помнить в голове:
- "Владеет ли этот объект целевым объектом?"
- "Может ли целевой объект быть удалён?"
class ViewControler: UIViewController {
let dataManager: DataManager
init() {
// Кто владеет кем? Это не очевидно
self.dataManager = DataManager(delegate: self)
}
}
Лучше быть явным:
class ViewController: UIViewController {
let dataManager: DataManager
init() {
self.dataManager = DataManager()
self.dataManager.delegate = self // Явно weak
}
}
Проблема 3: Сложно debug'ить
Когда приложение крашится на unowned ссылке, стек вызовов часто указывает на странное место.
Thread 1: EXC_BAD_ACCESS (code=1, address=0x...)
Frame 0: Pet.printOwnerName() + 42
Frame 1: ... [запутанный стек]
Это затрудняет поиск реальной проблемы.
Проблема 4: Неправильное использование в замыканиях
Unowned в capture list'е closure'а может привести к трудноуловимым ошибкам:
class ViewController: UIViewController {
var completion: (() -> Void)?
func setupCompletion() {
completion = { [unowned self] in
self.updateUI() // CRASH если ViewController был удалён
}
}
}
var vc: ViewController? = ViewController()
vc?.setupCompletion()
vc = nil // ViewController удалён
// Если кто-то вызовет completion — CRASH
Безопаснее:
completion = { [weak self] in
guard let self else { return }
self.updateUI()
}
Когда Unowned всё ещё используется
Unowned имеет несколько валидных use case'ов:
1. Очень чёткий parent-child relationship
class TreeNode {
var children: [TreeNode] = []
unowned let parent: TreeNode? // Родитель всегда существует
init(parent: TreeNode) {
self.parent = parent
parent.children.append(self)
}
}
Здесь мы АБСОЛЮТНО УВЕРЕНЫ что parent существует пока существует child.
2. В performance-critical коде
class HighPerformanceCache {
unowned let owner: GameScene // Нужна максимальная скорость
}
Но даже тут нужна осторожность.
Современный подход: Weak everywhere
В Swift 5.7+ рекомендуется просто использовать weak везде:
class Service {
weak var delegate: Delegate? // Просто weak
func execute() async {
guard let delegate = delegate else { return }
// Безопасно вызываем
await delegate.didFinish()
}
}
Альтернатива: Combine и Publisher'ы
Вместо delegate pattern'а с unowned/weak, используй Combine:
class Service {
let resultPublisher = PassthroughSubject<String, Never>()
func execute() async {
let result = try await fetchData()
resultPublisher.send(result)
// Нет проблем с lifecycle!
}
}
class ViewController: UIViewController {
let service = Service()
var cancellables: Set<AnyCancellable> = []
func setup() {
service.resultPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] result in // Даже если weak — нет крашей
self?.displayResult(result)
}
.store(in: &cancellables)
}
}
Вывод и рекомендации
ИЗБЕГАЙТЕ unowned потому что:
- Крашится приложение без warning'ов
- Сложно отследить в async коде
- Непредсказуемо в многопоточности
- Трудно debug'ить
- Нет преимуществ over weak
ИСПОЛЬЗУЙТЕ вместо этого:
weak varсguard letпроверками- Combine/Reactive паттерны
- Явное управление lifecycle'ом
- Dependency injection паттерны
В современном Swift:
- async/await делает unowned ещё более опасным
- weak является безопасным выбором по умолчанию
- Сложность управления памятью переместилась в async граничные условия
Если разработчик хочет использовать unowned, это должно быть с явным комментарием и абсолютной уверенностью в lifecycle'е.