Видеокурсы по изучению языка программирования Swift. Подробнее

Замена циклов сильных ссылок между экземплярами классов

Если вы нашли опечатку в тексте, выделите ее и нажмите CTRL + ENTER.

Swift предлагает два способа переопределить ссылку, чтобы она была не сильной, а слабой или бесхозной.

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

Используйте слабые ссылки, когда есть вероятность того, что в какой-то момент ссылка станет nil. И наоборот, используйте бесхозные ссылки, когда вы точно значете, что она никогда не станет nil, после того как будет инициализирована.

Слабые (weak) ссылки

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

Используйте слабые связи для избежания ссылочных циклов, когда есть вероятность того, что в какой-то момент времени ссылка будет иметь значение nil. Если ссылка будет всегда иметь значение, то используйте бесхозные ссылки, которые будут описаны далее. В примере с апартаментами выше, есть некоторая вероятность того, что апартаменты могут иметь “отсутствие” жильцов в какой-то момент времени, так что в нашем случае слабая ссылка подходит для разрыва цикла сильных связей.

Заметка

Слабые (weak) ссылки должны объявляться как переменные, что отображает изменчивость их значений с течением времени. Слабые ссылки не могут быть константами.

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

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

Пример ниже идентичен тому, что мы разбирали с вами с классами Person, Apartment, но только теперь в нем есть одно существенное отличие. В этот раз свойство tenant экземпляра класса Apartment объявлено как слабая ссылка:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) деинициализируется") }
}
 
class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("Apartment \(unit) деинициализируется") }
}

Создадим как и в предыдущем примере сильные ссылки от двух переменных (john, unit4A) и связи между двумя экземплярами:

var john: Person?
var unit4A: Apartment?
 
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
 
john!.apartment = unit4A
unit4A!.tenant = john

Вот как теперь выглядит соединение двух экземпляров между собой:

Экземпляр Person все еще имеет сильную ссылку на экземпляр Apartment, но Apartment имеет слабую (weak) ссылку на экземпляр Person. Это означает, что когда вы разрушаете сильную ссылку, которая содержится в переменной john, то больше сильных ссылок, указывающих на экземпляр Person, не остается:

john = nil
// Выведет "John Appleseed деинициализируется"

А так как больше сильных ссылок на экземпляр Person нет, то свойство tenant становится равным nil:

Остается только одна сильная ссылка на экземпляр Apartment из переменной unit4A. Если вы разрушите эту сильную ссылку, то их общее количество станет равным нулю:

unit4A = nil
// выводит "Апартаменты 4A деинициализируется"

А так как больше сильных ссылок нет, то и экземпляр Apartment тоже освобождается:

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

Заметка

Там, где используются сборщики "мусора", слабые указатели иногда используются для реализации простого механизма кеширования, потому что объекты без сильных связей сразу отпускаются, как только у памяти появляется необходимость избавится от "мусора". Однако со включенной ARC значения удаляются только тогда, когда уходит последняя сильная связь на них, делая слабые связи не подходящими для текущей задачи.

Бесхозные ссылки

Как и слабые ссылки, бесхозные ссылки так же не имеют сильной связи с экземпляром, на который они указывают. В отличии от слабых ссылок, бесхозные ссылки всегда имеют значение. Из-за этого бесхозные ссылки имеют неопциональный тип. Вы указываете на то, что ссылка бесхозная ключевым словом unowned, поставленным перед объявлением свойства или переменной.

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

Заметка

Если вы попытаетесь получить доступ к бесхозной ссылке после того, как экземпляр, на который она ссылается освобожден, то выскочит runtime ошибка. Используйте бесхозные ссылки только в том случае, если вы абсолютно уверены в том, что ссылка всегда будет указывать на экземпляр.

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

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

Взаимоотношения между Customer и CreditCard немного отличаются от предыдущего примера с Apartment и Person. В этом случае клиент может иметь или не иметь кредитной карты, но кредитная карта всегда имеет владельца. Чтобы это отобразить, класс 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) деинициализируется") }
}
 
class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Карта #\(number) деинициализируется") }
}

Заметка

Свойство number класса CreditCard определено как значение типа UInt64, а не Int, для того, чтобы оно было достаточно большим, чтобы хранить числа с 16 цифрами и на 32, и на 64 разрядных системах.

Следующий кусок кода определяет опциональную переменную типа Customer? с именем john, которая будет использоваться для хранения ссылки на определенного клиента. Эта переменная имеет начальное значение nil, в силу того, что это опциональный тип:

var john: Customer?

Вы можете создать экземпляр Customer и использовать его для инициализации и присваивания нового экземпляра CreditCard, как свойство клиентской card:

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234567890123456, customer: john!)

Вот как выглядят ссылки, после того как вы связали эти два экземпляра:

Экземпляр Customer имеет сильную ссылку на экземпляр CreditCard, а экземпляр CreditCard имеет бесхозную ссылку на экземпляр Customer.

Из-за того, что ссылка customer является бесхозной, то при разрушении сильной ссылки, которая находится в переменной john, больше сильных ссылок, указывающих на экземпляр Customer не остается:

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

john = nil
// Выведет "John Appleseed деинициализируется"
// Выведет "Карта #1234567890123456 деинициализируется"

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

Заметка

Примеры выше показывают как использовать safe unowned связи. Swift так же предоставляет unsafe unowned связи для случаев, где вам нужно отключить проверку безопасности во время исполнения, например в случае, когда вы хотите увеличить производительность. Как и со всеми небезопасными операциями, всю ответственность за проверку кода на безопасность вы берете на себя.

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

Бесхозные ссылки и неявно извлеченные опциональные свойства

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

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

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

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

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

Пример внизу определяет два класса 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.

Из всего этого можно сделать вывод, что вы можете создать экземпляры Country и City единственным выражением, без создания цикла сильных ссылок друг на друга. Получить значение свойства capitalCity можно напрямую без использования восклицательного знака для извлечения опционального значения:

var country = Country(name: "Россия", capitalName: "Москва")
print("Столицей страны \(country.name) является \(country.capitalCity.name)")
// Выведет "Столицей страны Россия является Москва"

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

 

Swift: 
4.0