Основы Swift / 14.2. Разрешение циклов сильных ссылок между объектами классов


Видео


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

Слабые и нелвадеющие ссылки позволяют одному объекту в цикле ссылок ссылаться на другой без удержания сильной ссылки на него. Объекты могут ссылаться друг на друга без создания цикла сильных ссылок.

Используйте слабую ссылку, когда другой объект имеет более короткое время жизни, что значит, что другой объект будет деалоцирован первым. В примере с Apartment выше будет верным для апартаментов иметь возможность не иметь постояльца в какой-то период их жизненного цикла, и так что слабая ссылка здесь является подходящим способом разбить цикл ссылок в данном случае. В противоположность, используйте невладеющие ссылки, когда другой объект имеет то же или большее время жизни.
Слабые ссылки
Слабая ссылка - это ссылка, которая не хранит сильно удержание на объект, на который ссылается, и таким образом не останавливает ARC от уничтожения этого ссылочного объекта. Это поведение препятствует ссылке стать частью цикла сильных ссылок. Вы указываете слабую ссылку, размещая ключевое слово weak перед объявлением свойства или переменной.

Так как слабая ссылка не держит сильной связи и объектом, на который ссылается, то возможно для этого объекта быть деалоцированным, когда слабая ссылка всё ещё ссылается на него. Следовательно, ARC автоматически установит слабую ссылку в nil, когда объект, на который идёт ссылка, будет деаллоцирован. И так как слабым ссылкам нужно иметь возможность менять свои значения на nil в процессе выполнения, то они всегда объявляются как переменные нежели константы опционального типа.

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

Наблюдатели свойств не вызываются, когда ARC устанавливает слабую ссылку в nil.
@{14.2\1\1}
Пример ниже идентичен примеру Person и Apartment выше с одним большим отличием. В этот раз тип свойства tenant Apartment объявлен как слабая ссылка:
class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) is being deinitialized") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}
@{14.2\1\2}
Сильные ссылки из двух переменных (john и unit4A) и ссылки между двумя объектами создаются как раньше:
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john
Здесь представлено, как выглядят ссылки между двумя объектами вместе:
@{14.2\1\3}
Объект Person всё ещё имеет сильную ссылку на объект Apartment, но объект Apartment теперь имеет слабую ссылку на объект Person. Это означает, что когда Вы разобьете строгую ссылку, удерживаемую переменной john, установкой в nil, то на объект Person больше не будет сильных ссылок:
john = nil
// Вывод "John Appleseed is being deinitialized"
Так как больше нет сильных ссылок на объект Person, то он будет деалоцирован, а свойство tenant установлено в nil:
@{14.2\1\4}
Единственная оставшаяся сильная ссылка на объект Apartment идёт из переменной unit4A. Если Вы разобьёт эту сильную ссылку, то не останется больше сильных сырок на объект Apartment:
unit4A = nil
// Выведет "Apartment 4A is being deinitialized"
Так как больше нет сильных ссылок на объект Apartment, то он так же будет деаллоцирован:
А сборка мусора в другом замке
В системах, которые используют сборку мусора, слабые указатели часто используются для реализации простого механизма кэширования, так как объекты без сильных ссылок деалоцируются только тогда, когда давление памяти вызывает запуск сборки мусора. Однако с помощью ARC значения деаллоцируются сразу, как только последняя сильная ссылка на них исчезает, что делает слабые ссылки непригодными для этой цели.
Невладеющие ссылки
Как и слабые ссылки, невладеющие ссылки не содержат сильной привязки к объекту, на который они ссылаются. В отличие от слабых ссылок, однако, невладеющие ссылки используются, когда другие объекты имеют то же время жизни иди большее. Вы указываете невладеющие ссылки с помощью размещения ключевого слова unowned перед объявлением свойства или переменной.

Предполагается, что невладеющая ссылка всегда имеет значение. В результате ARC никогда не установит значение невладеющей ссылки в nil, что значит, что невладеющие ссылки определяются с использованием неопциональных типов.

Используйте невладеющие ссылки, только когда Вы уверены, что ссылка всегда указывает на объект, который не был деаллоцирован.

Если Вы попытаетесь получить доступ к объекту по невладеющей ссылке после того, как этот объект был деаллоцирован, то Вы получит ошибку времени выполнения.
@{14.2\2\1}
Следующий пример определяет два класса Customer и CreditCard, которые моделируют пользователя банка и возможную кредитную карту для него. Эти два класса оба содержат объект другого в качестве свойства. Это отношение потенциально может создать сильный цикл ссылок.

Отношения между Customer и CreditCard отличны от тех, что были между Apartment и Person, которые можно было увидеть в примере со слабой ссылкой выше. В этой модели данных клиент может иметь, а может не иметь кредитной карты, но кредитная карта всегда связана с клиентом. Объект CreditCard никогда не переживает объект Customer, на который ссылается. Для представления этого класс Customer имеет опциональное свойство card, но класс CreditCard имеет невладеющее (и неопциональное) свойство customer.

Помимо того новый объект CreditCard может быть создан только передачей значения number и объекта Customer в специальный инициализатор CreditCard. Это гарантирует, что объект CreditCard всегда будет иметь связанный с ним объект customer, когда CreditCard будет создан.

Ввиду того, что кредитная карта всегда имеет клиента, Вы определяете свойство customer в качестве невладеющего для избежания сильного цикла ссылок:
class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}
Свойство number класса CreditCard определено с типом UInt64 вместо Int для того, чтобы быть уверенными, что вместимость свойства number достаточно объемно, чтобы хранить 16-значный номер карты и на 32-битной, и на 64-битной системах.
@{14.2\2\2}
Следующий пример кода определяет переменную Customer? с названием john, которая будет использована для хранения ссылки на конкретного клиента. Эта переменная имеет начально значение nil, что достигается её опциональностью:
var john: Customer?
@{14.2\2\3}
Теперь Вы можете создать объект Customer и использовать его для инициализации и присваивания нового объекта CreditCard его свойству card:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
Здесь представлено то, как выглядят эти ссылки: Объект Customer теперь имеет сильную ссылку на объект CreditCard, а объект CreditCard теперь имеет невладеющую ссылку на объект Customer.

Ввиду невладеющей ссылки customer когда Вы разрушите сильную ссылку от переменной john, то не останется больше сильных ссылок на объект Customer:
@{14.2\2\4}
Так как больше нет сильных ссылок на объект Customer, то он будет деаллоцирован. После того, как это произойдёт, то не будет больше сильных ссылок на объект CreditCard, а значит он тоже будет деаллоцирован:
john = nil
// Выведет "John Appleseed is being deinitialized"
// Выведет "Card #1234567890123456 is being deinitialized"
Последний пример кода выше показывает, что деинициализаторы для объекта Customer и CreditCard оба выводят сообщения "deinitialized" после того, как переменная john установлена в nil.

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

Вы указываете небезопасную невладеющую ссылку с помощью написания unowned(unsafe). Если Вы попытаетесь получить доступ к небезопасной невладеющей ссылке после того, как объект, на который он ссылается, будет деаллоцирован, Ваша программа попытается получить доступ к локации памяти, где объект мог бы быть, что является небезопасной операцией.
Невладеющие ссылки и неявно извлекаемые опциональные свойства
Пример для слабых и невладеющих ссылок выше покрывает два из двух более общих сценария, в которых необходимо разбить цикл сильных ссылок.

Примеры Person и Apartment демонстрируют ситуацию, где два свойства, каждому из которых разрешено быть nil, имеют потенциальную возможность создать цикл сильных ссылок. Этот сценарий лучше всего разрешается с помощью слабых ссылок.

Пример Customer и CreditCard показывают ситуацию, когда одному свойству разрешено быть nil, а другому это не позволено, и оба этих свойства имеют возможность создать цикл сильных ссылок. Такой сценарий лучше всего разрешается с помощью невладеющих ссылок.

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

Это позволяет обоим свойствам иметь возможность прямого доступа (без опционального извлечения) после завершения инициализации, с избежание цикла ссылок. Эта секция демонстрирует Вам, как задать такие взаимоотношения.
@{14.2\3\1}
Пример ниже задаёт два класса, Country и City, каждый из которых хранит объект другого класса в качестве свойства. В этой модели данных каждая страна должна иметь столичный город, и каждый город должен принадлежать стране. Чтобы представить это, класс Country имеет свойство capitalCity, а класс City имеет свойство country:
class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}
Чтобы установить взаимную зависимость между двумя классами, инициализатор для City принимает экземпляр Country и сохраняет его в качестве свойства country.

Инициализатор для City вызывается изнутри инициализатора Country. Однако инициализатор Country не может передать self в инициализатор City до тех пор, пока объект Country не будет полностью проинициализирован, как это описано в {Двухфазовой инициализации}.

Чтобы соблюсти это требование, Вы можете объявить свойство capitalCity у Country в качестве неявно извлекаемого опционального свойства, указываемого с помощью восклицательного знака в конце аннотации типа (City!). Это означает, что свойство capitalCity имеет значение по-умолчанию nil, как и любой другой опционал, но он может быть использован без необходимости распаковывать его значение.

Так как capitalCity имеет значение по-умолчанию nil, то новый объект Country будет полностью проинициализирован, как только объект Country установить своё свойство name изнутри инициализатора. Это означает, что инициализатор Country может начать ссылаться и передавать неявное свойство self, как только свойство name будет задано. Затем инициализатор Country может передать self в качестве одного из параметров для инициализатора City, когда инициализатор Country установить своё собственное свойство capitalCity.
@{14.2\3\2}
Всё это значит, что Вы можете создать объекты Country и City одной инструкцией без создания цикла сильных ссылок, а свойство capitalCity можно использовать напрямую без необходимости применения восклицательного знака для извлечения опционального значения:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Выведет "Canada's capital city is called Ottawa"
В примере выше использование неявно извлекаемого опционала означает, что все требования для двухфазных инициализаторов класса выполняются. Свойство capitalCity может быть использовано как и неопциональное значение, когда инициализация завершится, хотя так же избегается, как и ранее, цикл сильных ссылок.