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

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

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

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

Swift: 
3.0