Перейти к содержимому
Теория · Память и ARC · урок 4 из 7

weak и unowned

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

В прошлом уроке человек и квартира держали друг друга, и оба объекта утекали. Чтобы цикла не было, одна из двух ссылок должна быть такой записью в каталоге: знать об объекте, но не держать его.

SWIFT
class Person {
    var home: Apartment?
    deinit { print("Person freed") }
}
class Apartment {
    weak var tenant: Person?
}

var anna: Person? = Person()
let flat = Apartment()
anna?.home = flat
flat.tenant = anna
anna = nil // Person freed
print(flat.tenant == nil) // true

Давайте разберём программу:

  1. weak var tenant — квартира знает жильца, но не держит его: эта ссылка не прибавляет единицу к счётчику Person.
  2. После flat.tenant = anna у человека одна сильная ссылка — anna.
  3. anna = nil убирает её. Счётчик стал нулём, срабатывает deinit, и программа печатает Person freed.
  4. Swift сам записывает nil в flat.tenant, поэтому print печатает true.

Цикла больше нет: человек держит квартиру, а квартира человека — нет.

Ссылка weak

weak-ссылка — ссылка, которая не держит объект: она не прибавляет единицу к его счётчику. Когда объект освобождается, Swift сам записывает в неё nil.

Как с ней работают:

  • перед объявлением пишут слово weak: weak var tenant: Person?;
  • тип у неё всегда опциональный, ведь туда должен поместиться nil;
  • читают её как любой опционал: через if let, ?. или сравнение с nil.

Без ? компилятор не примет объявление:

TEXT
error: 'weak' variable should have optional type 'Person?'

Поэтому weak-ссылка безопасна: если объекта уже нет, вы прочитаете nil.

Какую ссылку сделать weak

Из двух ссылок цикла weak делают одну, и выбор не случаен. Спросите себя, кто кем владеет:

  • сильная ссылка идёт от владельца к тому, чем он владеет: человек держит свою квартиру;
  • обратная ссылка, к владельцу, делается weak: квартира только знает жильца.

Так же устроена пара «родитель — ребёнок»: родитель держит ребёнка сильной ссылкой, а ребёнок знает родителя через weak. Если сделать наоборот, ребёнка никто не будет держать, и он освободится раньше времени.

Ссылка unowned

Бывает, что объект точно живёт дольше ссылки на него. У банковской карты всегда есть владелец, и без него карта не существует. Для такой ссылки есть второй вид — unowned-ссылка. Она тоже не держит объект, но Swift не записывает в неё nil, поэтому её обычно объявляют без ?.

SWIFT
class Customer {
    let name: String
    init(name: String) { self.name = name }
}
class Card {
    unowned let owner: Customer
    init(owner: Customer) { self.owner = owner }
}

var alice: Customer? = Customer(name: "Alice")
let card = Card(owner: alice!)
print(card.owner.name) // Alice

Разберём, что здесь происходит:

  1. unowned let owner: Customer — карта знает владельца, но не держит его.
  2. Тип без ?, поэтому card.owner.name читают напрямую, без if let и ?..
  3. Владельца держит переменная alice, и пока она его держит, карта работает.

С unowned удобнее, но опаснее. Если владельца всё-таки освободили раньше карты, читать такую ссылку нельзя:

SWIFT
alice = nil
print(card.owner.name) // ✗

Программа аварийно завершится: Swift сообщит, что прочитали unowned-ссылку на уже уничтоженный объект. Поэтому unowned берут, только когда уверены, что объект переживёт ссылку. Если сомневаетесь, берите weak.

Здесь картинка с каталогом заканчивается. weak похожа на запись в каталоге, которую стирают при сдаче книги. А unowned — бумажка, которую не стирает никто: если книгу уже сдали, пойти по бумажке — значит аварийно завершить программу.

На собеседовании часто спрашивают, чем weak отличается от unowned. Ответ можно строить так:

  • обе ссылки не держат объект и поэтому разрывают цикл;
  • weak — опционал: когда объект освобождён, в ней nil, и читать её безопасно;
  • unowned в nil не превращается, и если прочитать её после освобождения объекта, программа аварийно завершится;
  • unowned берут, только когда объект точно живёт дольше ссылки.

Частая ошибка

Новый объект положили прямо в weak-ссылку:

SWIFT
let studio = Apartment()
studio.tenant = Person() // ✗
print(studio.tenant == nil) // true
TEXT
warning: instance will be immediately deallocated because property 'tenant' is 'weak'

Компилятор предупреждает: на новый объект нет ни одной сильной ссылки, а weak его не держит. Объект освобождается на той же строке: deinit печатает Person freed, и tenant сразу становится nil. Сначала положите объект в сильную ссылку, например let carl = Person(), а потом уже в studio.tenant.

Главное

  • weak и unowned не держат объект: они не прибавляют единицу к счётчику и поэтому разрывают цикл.
  • weak-ссылка опциональная: когда объект освобождён, Swift записывает в неё nil.
  • weak делают обратную ссылку, к владельцу: ребёнок знает родителя через weak, а родитель держит ребёнка сильной ссылкой.
  • unowned не становится nil: если прочитать её после освобождения объекта, программа аварийно завершится, поэтому unowned берут, только когда объект живёт дольше ссылки.