Документация

Автоматический подсчет ссылок (ARC)

Автоматический подсчет ссылок (ARC)

Swift использует automatic reference counting (автоматический подсчет ссылок) для отслеживания и управления памятью вашего приложения. В большинстве случаев это означает, что управление памятью "просто работает" в Swift и вам не нужно думать о самостоятельном управлении памятью. ARC автоматически освобождает память, которая использовалась экземплярами класса, когда эти экземпляры больше нам не нужны.

Однако, в некоторых случаях для управления памятью ARC нужно больше информации об отношениях между некоторыми частями вашего кода. Эта глава опишет эти случаи и покажет как включить ARC, чтобы эта система взяла на себя весь контроль памятью вашего приложения. Использование ARC в Swift очень схоже с использованием ARC в Objective-C, описание которого можно найти в Transitioning to ARC Release Notes.

ARC применима только для экземпляров класса. Структуры и перечисления являются типами значений, а не ссылочными типами, и они не хранятся и не передают свои значения по ссылке.

Работа ARC

Каждый раз, когда вы создаете экземпляр класса, ARC выделяет фрагмент памяти для хранения информации этого экземпляра. Этот фрагмент памяти содержит информацию о типе экземпляра, о его значении и любых свойствах хранения, связанных с ним.

Дополнительно, когда экземпляр больше не нужен, ARC освобождает память, использованную под этот экземпляр, и направляет эту память туда, где она нужна. Это своего рода гарантия того, что ненужные экземпляры не будут занимать память.

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

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

Для того чтобы это было возможно, каждый раз как вы присваиваете экземпляр свойству, константе или переменной создается strong reference (сильная ссылка) с этим экземпляром. Такая связь называется “сильной”, так как она крепко держится за этот экземпляр и не позволяет ему освободится до тех пор, пока остаются сильные связи.

ARC в действии

Приведем пример того, как работает ARC. Наш пример начнем с класса Person, который определяет константное свойство name:

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

Класс Person имеет инициализатор, который устанавливает name свойство экземпляра и выводит сообщение для отображения того, что идет инициализация. Так же класс Person имеет деинициализатор, который выводит сообщение, когда экземпляр класса освобождается.

Следующий фрагмент кода определяет три переменные класса Person?, который используется для установки нескольких ссылок к новому экземпляру Person в следующих кусках кода. Так как эти переменные опционального типа (Person?, а не Person), они автоматически инициализируются со значением nil, и не имеют никаких ссылок на экземпляр Person.

var reference1: Person?
var reference2: Person?
var reference3: Person?

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

reference1 = Person(name: "John Appleseed")
// Выведет "John Appleseed инициализируется"

Обратите внимание, что сообщение "John Appleseed инициализируется" выводится во время того, как вы вызываете инициализатор класса Person. Это подтверждает тот факт, что происходила инициализация.

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

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

reference2 = reference1
reference3 = reference1

Теперь экземпляр класса Person имеет три сильные ссылки.

Если вы сломаете две из этих трех ссылок (включая и первоначальную ссылку), присвоив nil двум переменным, то останется одна сильная ссылка, и экземпляр Person не будет освобожден:

reference1 = nil
reference2 = nil

ARC не освободит экземпляр класса Person до тех пор, пока остается последняя сильная ссылка, уничтожив которую мы укажем на то, что наш экземпляр больше не используется:

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

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

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

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

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

Ниже приведен пример того, как сильный ссылочный цикл может быть создан по ошибке. В этом примере мы определяем два класса Person и 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 }
    var tenant: Person?
    deinit { print("Апартаменты \(unit) освобождаются") }
}

Каждый экземпляр Person имеет свойство name типа String и опциональное свойство apartment, которое изначально nil. Свойство apartment опционально, так как наша персона не обязательно всегда должна иметь апартаменты.

Аналогично, что каждый экземпляр Apartment имеет свойство unit типа String и опциональное свойство tenant, которое изначально nil. Свойство tenant опциональное, потому как не всегда в апартаментах кто-то живет.

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

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

var john: Person?
var unit4A: Apartment?

Теперь вы можете создать свои экземпляры Person и Apartment и присвоить их этим переменным john, unit4A:

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

Вот как выглядят сильные связи после того создания и присваивания этих двух экземпляров. Переменная john имеет сильную связь с экземпляром класса Person, переменная unit4A имеет сильную связь с экземпляром Apartment:

Теперь вы можете соединить эти два экземпляра вместе, так что житель будет иметь апартаменты, а апартаменты будут иметь своих жителей. Обратите внимание, что восклицательный знак (!) используется для развертывания и допуска к экземплярам, хранимым в опциональных переменных john, unit4A, так что установить значения свойством можно в такой форме:

john!.apartment = unit4A
unit4A!.tenant = john

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

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

john = nil
unit4A = nil

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

Вот как выглядят сильные ссылки после того, как вы присвоили nil переменным, john, unit4A:

Сильные взаимные ссылки остались между экземплярами Person и Apartment и не могут быть разрушены.

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

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

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

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

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

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

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

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

Заметка

Когда 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 тоже освобождается:

Заметка

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

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

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

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

Заметка

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

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

Следующий пример определяет два класса 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 ссылке после того, как экземпляр был освобожден, ваша программа попытается получить доступ к памяти, где этот объект хранился ранее, что само по себе является небезопасной операцией.

Бесхозные опциональные ссылки

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

Ниже приведен пример, который показывает курсы, которые в свою очередь может предложить департамент в школе:

class Department {
    var name: String
    var courses: [Course]
    init(name: String) {
        self.name = name
        self.courses = []
    }
}

class Course {
    var name: String
    unowned var department: Department
    unowned var nextCourse: Course?
    init(name: String, in department: Department) {
        self.name = name
        self.department = department
        self.nextCourse = nil
    }
}

Department держит сильную ссылку на каждый курс, который предлагает департамент. В модели владения ARC департамент владеет всеми курсами, что предлагает для прохождения. Сам тип Course имеет две бесхозные ссылки: одна на департамент, другая - на следующий курс, но сам курс не владеет ни одним из этих объектов. Каждый курс является частью департамента, так что свойство departament не является опциональным. Однако, некоторые курсы не имеют следующего курса, так что свойство nextCourse является опциональным.

Вот пример использования этих классов:

let department = Department(name: "Horticulture")

let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]

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

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

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

Заметка

Лежащий в основе опционального значения тип - Optional, который является по своей сути просто перечислением в стандартной библиотеке Swift. Однако, опционалы являются исключением из правил, так как типы значений не могут быть маркированы как 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 может быть использовано как неопциональное значение, после того как инициализация закончена, все так же избегая цикла сильных ссылок.

Циклы сильных ссылок в замыканиях

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

Сильные ссылки так же могут образовываться, когда вы присваиваете замыкание свойству экземпляра класса, и тело замыкания захватывает экземпляр. Этот захват может случиться из-за того, что тело замыкания получает доступ к свойству экземпляра, например self.someProperty, или из-за того, что замыкание вызывает метод типа self.someMethod(). В обоих случаях эти доступы и вызывают тот самый “захват” self, при этом создавая цикл сильных ссылок.

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

Swift предлагает элегантное решение этой проблемы, которые известно как список захвата замыкания (closure capture list). Однако до того, как вы узнаете, как разрушить такой цикл с помощью этого решения, давайте разберемся, что этот цикл может вызвать.

Пример ниже отображает, как вы можете создать цикл сильных ссылок, когда мы используем замыкание, которое ссылается на self. В этом примере определяем класс HTMLElement, который представляет модель простого элемента внутри HTML документа:

class HTMLElement {
 
    let name: String
    let text: String?
 
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
 
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
 
    deinit {
        print("\(name) деинициализируется")
    }
}

Класс HTMLElement определяет свойство name, которое отображает имя элемента, например "p" тег для отображения параграфа или “br” для тэга перехода на следующую строку. Класс HTMLElement также определяет опциональное свойство text, которому может быть присвоена строка, которая отображает текст, который может быть внутри HTML элемента.

В дополнение к этим двум простым свойствам класс HTMLElement определяет ленивое свойство asHTML. Это свойство ссылается на замыкание, которое комбинирует name, text во фрагмент HTML строки. Свойство asHTML имеет тип () -> String, или другими словами функция, которая не принимает параметров и возвращает строку.

По умолчанию свойству asHTML присвоено замыкание, которое возвращает строку, отображающую тэг HTML. Этот тэг содержит опциональный text, если таковой есть или не содержит его, если text, соответственно, отсутствует. Для элемента параграфа замыкание вернет “<p>some text</p>” или просто “<p />”, в зависимости от того, имеет ли свойство text какое либо значение или nil.

Свойство asHTML называется и используется несколько схоже с методом экземпляра. Однако из-за того что asHTML является свойством-замыканием, а не методом экземпляра, то вы можете заменить значение по умолчанию свойства asHTML на пользовательское замыкание, если вы хотите сменить отображение конкретного HTML элемента.

Например, свойству asHTML может быть присвоено замыкание, которое имеет дефолтный текст на случай если свойство text равно nil, для предотвращения отображения пустого HTML тега:

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
   return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Выведет "<h1>some default text</h1>"

Заметка

Свойство asHTML объявлено как ленивое свойство, потому что оно нам нужно только тогда, когда элемент должен быть отображен в виде строкового значения для какого-либо HTML элемента выходного значения. Факт того, что свойство asHTML является ленивым, означает, что вы можете ссылаться на self внутри дефолтного замыкания, потому что обращение к ленивому свойству невозможно до тех пор, пока инициализация полностью не закончится и не будет известно, что self уже существует.

Класс HTMLElement предоставляет единственный инициализатор, который принимает аргумент name и (если хочется) аргумент text для инициализации нового элемента. Класс также определяет деинициализатор, который выводит сообщение, для отображения момента когда экземпляр HTMLElement освобождается.

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

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Выведет "<p>hello, world</p>"

Заметка

Переменная paragraph определена как опциональный HTMLElement, так что он может быть и nil для демонстрации цикла сильных ссылок.

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

Свойство asHTML экземпляра держит сильную ссылку на его замыкание. Однако из-за того, что замыкание ссылается на self внутри своего тела (self.name, self.text), оно захватывает self, что означает, что замыкание держит сильную ссылку обратно на экземпляр HTMLElement. Между ними двумя образуется цикл сильных ссылок. (Для более подробной информации по захвату значений в замыканиях читайте соответствующий раздел Захват значений.)

Заметка

Даже несмотря на то, что замыкание ссылается на self несколько раз, оно захватывает лишь одну сильную ссылку на экземпляр HTMLElement.

Если вы установите значение paragraph на nil, чем разрушите сильную ссылку на экземпляр HTMLElement, то ни экземпляр HTMLElement, ни его замыкание не будут освобождены из-за цикла сильных ссылок:

paragraph = nil

Обратите внимание, что сообщение деинициализатора HTMLElement не выводится на экран, что и есть факт того, что этот экземпляр не освобожден.

Замена циклов сильных ссылок в замыканиях

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

Заметка

Swift требует от вас написания self.someProperty или self.someMethod() (вместо someProperty, someMethod()), каждый раз, когда вы обращаетесь к члену свойства self внутри замыкания. Это помогает вам не забыть, что возможен случай случайного захвата self.

Определение списка захвата

Каждый элемент в списке захвата является парой ключевого слова weak или unowned и ссылки на экземпляр класса (например, self) или переменную, инициализированную с помощью какого-либо значения (например, delegate = self.delegate!). Эти пары вписываются в квадратные скобки и разделяются между собой запятыми.

Размещайте список захвата перед списком параметров замыкания и его возвращаемым типом:

lazy var someClosure: (Int, String) -> String = {
      [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
   // тело замыкания
}

Если у замыкания нет списка параметров или возвращаемого типа, так как они могут быть выведены из контекста, то разместите список захвата в самом начале замыкания, перед словом in:

lazy var someClosure: () -> String = {
      [unowned self, weak delegate = self.delegate!] in
    // тело замыкания
}

Слабые (weak) или бесхозные (unowned) ссылки

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

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

Заметка

Если захваченная ссылка никогда не будет nil, то она должна быть всегда захвачена как unowned ссылка, а не weak ссылка.

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

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) освобождается")
    }
}

Эта реализация HTMLElement идентична предыдущей реализации, кроме дополнения списка захвата внутри замыкания asHTML. В этом случае список захвата [unowned self], который означает: “захватить self как unowned ссылку, вместо strong”.

Вы можете создать и вывести экземпляр HTMLElement как и раньше:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Выведет "<p>hello, world</p>"

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

В этот раз захват self является бесхозной ссылкой и уже не поддерживает сильной связи с экземпляром HTMLElement, которого он захватил. Если вы установите сильную ссылку от переменной paragraph на значение nil, то экземпляр HTMLElement будет освобожден, что можно определить по выводимому сообщению в примере ниже:

paragraph = nil
// Выведет "p освобождается"

Более подробную информацию можно найти в разделе "Список захвата".

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: