Основы Swift / 14.3. Циклы сильных ссылок для замыканий


Видео


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

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

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

Swift предоставляет элегантный способ решения этой проблемы, известный как лист захвата замыкания. Однако прежде, чем Вы узнаете, как разбить цикл сильных ссылок с помощью листа захвата замыкания, будет полезно понять, как такой цикл может возникнуть.
@{14.3\1\1}
Пример ниже демонстрирует, как Вы можете создать цикл сильных ссылок, когда Вы используете замыкание со ссылкой на self. Этот пример демонстрирует класс с названием HTMLElement, который предоставляет простую модель для отдельного элемента внутри документа HTML:
class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}
Класс HTMLElement определяет свойство name, которое определяет имя элемента, как например "h1" для заголовочного элемента, "p" для элемента параграфа или "br" для элемента разрыва строки. HTMLElement также определяет опциональное свойство text, которое Вы можете задать в строку, которая представляет текст, который будет отрендерен элементом HTML.

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

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

Свойство asHTML именуется и используется, как и метод объекта. Однако так как asHTML - это свойство-замыкание, а не метод объекта, то Вы можете заменить значение по-умолчанию для этого свойства asHTML другим замыканием, если Вы хотите изменить механизм сборки HTML для отдельного элемента HTML.
@[14.3\1\2}
Например, свойство asHTML могло бы получить замыкание, устанавливающее какой-то текст по-умолчанию, если свойство text равно nil, чтобы предотвратить возвращение пустого тэга HTML:
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)"
}
print(heading.asHTML())
// Выведет "<h1>some default text</h1>"
Свойство asHTML объявлено как ленивое свойство, так как оно нужно лишь тогда, когда элементу нужно фактически быть собранным в качестве строкового значения для конкретного выходного HTML. Тот факт, что asHTML - это ленивое свойство, значит, что Вы можете ссылаться на self внутри замыкания по-умолчанию, так как ленивое свойство не будет использовано до тех пор пока инициализация не будет завершена и self не будет гарантировано существовать.

Класс HTMLElement предоставляет единственный конструктор, который принимает аргумент name и (если нужно) аргумент text для создания нового элемента. Класс также определяет деструктор, который выводит сообщение, чтобы показать, когда HTMLElement будет деаллоцирован.
@{14.3\1\3}
Здесь Вы можете увидеть, как использовать 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.
@{14.3\1\4}
Если Вы установите переменную paragraph в nil и разобъёте его сильную ссылку на объект HTMLElement, то ни HTMLElement объект, ни замыкание не будут деаллоцированы, так как будет создан цикл сильных ссылок:
paragraph = nil
Заметьте, что сообщение в деинициализаторе HTMLElement не выводится, что демонстрирует то, что HTMLElement объект не будет деаллоцирован.
Разбитие циклов сильных ссылок для замыканий
Вы можете разбить цикл сильных ссылок между замыканием и объектом класса, определив лист захвата в качестве части определения замыкания. Список захвата определяет правила для захвата одного или более ссылочных типов внутри тела замыкания. Как и в случае с циклами сильных циклов между двумя объектами классов Вы можете объявить каждую захватываемую ссылку как слабую или невладеющую, а не сильную. Верный выбор слабой или невладеющей ссылке основывается на взаимоотношениях между частями Вашего кода.

Swift требует от Вас записывать self.someProperty или self.someMethod() (а не просто someProperty или someMethod()), когда Вы ссылаетесь на члена self из замыкания. Этот помогает Вам помнить, что существует возможность захватить self по ошибке.
Задание списка захвата
Каждый элемент в списке захвата - это пара из ключевого слова weak или unowned с ссылкой на объект класса (например, self) или переменную, инициализированную каким-то значением (как например, delegate = self.delegate!). Эти пары пишутся внутри пары квадратных скобок, разделённых запятыми.
@{14.3\2}
Разместите лист захвата перед списком параметров замыкания и возвращаемым типом, если они предоставлены:
lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
    // здесь будет тело замыкания
}
@{14.3\3}
Если замыкание не специфицирует список параметров или возвращаемый тип, так как они могут быть выведены из контекста, то разместите список захвата в самом начале замыкания, сопроводив его ключевым словом in:
lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // здесь будет тело замыкания
}
Слабые и невладеющие ссылки
Определите захват в замыкании в виде невладющей ссылки, когда замыкание и его объект ссылаются и будут всегда ссылаться друг на друга, а также будут деаллоцированы в одно и то же время.

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

Если захваченная ссылка никогда не станет nil, то её всегда следует захватывать в качестве невладеющей ссылки, а не слабой.
@{14.3\4\1}
Невладеющая ссылка - это подходящий метод захвата для разбития цикла сильных ссылок из раннего примера 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)"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}
Эта реализация класса HTMLElement идентична предыдущей реализации за исключением добавления списка захвата внутри замыкания asHTML. В этом случае лист захвата [unowned self], который означает "захватить self в качестве невладеющей ссылки, а не сильной".
@{14.3\4\2}
Вы можете создать и вывести объект класса HTMLElement как и раньше:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Выведет "<p>hello, world</p>"
Здесь Вы можете увидеть, как ссылки выглядят ввиду применения листа захвата:
@{14.3\4\3}
На этот раз захват self замыканием происходит по невладеющей ссылке и не создаёт сильного удержания объекта HTMLElement, который он захватывает, если Вы установите значение сильной ссылки переменной paragraph в nil, то объект HTMLElement будет деаллоцирован, что можно увидеть по выводу сообщения его деинициализатором в примере ниже:
paragraph = nil
// Выведет "p is being deinitialized"