Link Search Menu Expand Document

Автоматичний підрахунок посилань

У Swift для відслідковування й керування використанням пам’яті ваших додатків використовується механізм автоматичного підрахунку посилань (Automatic Reference Counting, або ARC). В більшості випадків, це означає що управління пам’яттю у Swift “просто працює”, і вам не потрібно замислюватись про управління пам’яттю самостійно. ARC автоматично звільняє пам’ять, що використовується екземплярами класів, коли ці екземпляри більше не потрібні.

Однак, у невеликій кількості випадків ARC вимагає додаткової інформації про взаємозв’язки між частинами вашого коду для автоматичного керування пам’яттю. У даному розділі описуються ці ситуації, і показано, як ви можете дати можливість ARC керувати всією пам’яттю вашого додатку. Використання ARC у Swift є дуже подібним до підходу, описаного в статті Transitioning to ARC Release Notes for using ARC для Objective-C.

Примітка

Reference counting only applies to instances of classes. Structures and enumerations are value types, not reference types, and are not stored and passed by reference.

Як працює ARC

Щоразу при створенні нового екземпляру класу, ARC виділяє блок пам’яті для зберігання інформації про цей екземпляр. У цьому блоці зберігається інформація про тип екземпляру, а також значення всіх властивостей, що зберігаються, асоційованих із даним екземпляром.

Додатково, коли екземпляр більше не потрібен, ARC звільнює пам’ять, котра використовувалась даним екземпляром, щоб її можна було використовувати для інших потреб. Це гарантує, що екземпляри класів не займають місце в пам’яті, коли вони більше не потрібні.

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

Щоб гарантувати, що екземпляри не зникають, поки вони ще потрібні, ARC відслідковує, скільки властивостей, констант та змінних в даний момент посилаються на кожен екземпляр класу. 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: "Дмитро Клюшин")
// Надрукує "Дмитро Клюшин ініціалізується"

Слід зауважити, що повідомлення "Дмитро Клюшин ініціалізується" друкується в момент виклику ініціалізатора класу Person. Це підтверджує факт ініціалізації.

Оскільки новий екземпляр Person було присвоєно змінній reference1, тепер є сильне посилання змінної reference1 на новий екземпляр Person. Оскільки є хоча б одне сильне посилання, ARC тримає цей екземпляр Person у пам’яті та не деалокує його.

Якщо присвоїти той же екземпляр Person двом іншим змінним, буде утворено ще два сильних посилання на цей екземпляр:

reference2 = reference1
reference3 = reference1

Тепер є три сильних посилання на цей єдиний екземпляр Person.

Якщо прибрати два з цих трьох сильних посилань (включно з початковим посиланням) шляхом присвоєння nil двом змінним, одне сильне посилання залишиться, і екземпляр Person не буде деалоковано:

reference1 = nil
reference2 = nil

ARC не деалокуватиме екземпляр Person допоки не буде прибрано третє й останнє сильне посилання, і в цей момент стає ясно, що екземпляр Person більше ніде не використовується:

reference3 = nil
// Надрукує "Дмитро Клюшин деініціалізується"

Цикли сильних посилань між екземплярами класів

У прикладах вище, ARC дозволяє відслідковувати кількість посилань на новий екземпляр Person та деалокувати екземпляр Person, коли він більше не потрібен.

Однак, можливо написати код, у котрому екземпляр класу ніколи не добирається до точки, коли він має нуль сильних посилань. Це може статись, коли два екземпляри класу тримають сильні посилання один на одного, таким чином тримаючи один одного живим. Такі ситуації називають циклами сильних посилань.

Цикли сильних посилань можна вирішити шляхом використання слабких (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 є опціональним, бо помешкання не завжди має мешканця.

Обидва цих класи мають ініціалізатори, котрі друкують повідомлення для підтвердження факту деініціалізації їх екземплярів. Це дозволяє бачити, чи дійсно екземпляри класів Person та Apartment деалокуються так, як ми цього очікуємо.

У наступному фрагменті коду оголошено дві змінні на ім’я 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, а він у свою чергу має сильне посилання на екземпляр Person. Ба більше, якщо прибрати сильні посилання, що тримають змінні john та unit4A, лічильники посилань не знизяться до нуля, і дані екземпляри не будуть деалоковані ARC:

john = nil
unit4A = nil

Жоден з деініціалізаторів не буде викликано при присвоєнні цим двом змінним значення nil. Цикл сильних посилань перешкоджає екземплярам Person та Apartment бути будь-коли деалокованими, спричиняючи витік пам’яті у даному додатку.

Ось так виглядають сильні посилання після того, як змінним john та unit4A було присвоєно значення nil:


Цикл сильних посилань між екземпляром Person та екземпляром Apartment житиме, і його ніяк не можна розв’язати.

Вирішення циклів сильних посилань між екземплярами класів

У Swift є два способи вирішувати цикли сильних посилань при роботі з властивостями типів-класів: слабкі посилання та безхазяйні посилання.

Слабкі та безхазяйні посилання дозволяють одному екземпляру в циклі посилатись на інший екземпляр не утримуючи його сильно. Екземпляри таким чином можуть посилатись один на одного не утворюючи при цьому циклу сильних посилань.

Слабкі посилання слід створювати тоді, коли інший екземпляр має коротший час існування; іншими словами, коли інший екземпляр може бути деалоковано раніше. У прикладі з помешканням вище, для помешкання нормально не мати мешканця в якийсь момент його існування, тому слабке посилання є доречним способом розірвати циклічне посилання у даному випадку. Безхазяйні посилання слід застосовувати у протилежних випадках: коли інший екземпляр має такий же або довший час існування.

Слабкі посилання

Слабим посиланням є посилання, котре не тримається міцно за екземпляр, на котрий воно посилається, і таким не заважає 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("Помешкання \(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 тепер має слабке посилання на екземпляр Person. Це означає, що при розірванні сильного посилання внаслідок присвоєння змінній john значення nil, більше не залишиться сильних посилань на екземпляр Person:

john = nil
// Надрукує "John Appleseed деініціалізується"

Оскільки більше немає сильних посилань на екземпляр Person, його буде деалоковано і властивості tenant буде задано значення nil:

Тепер єдиним сильним посиланням на екземпляр Apartment лишається змінна unit4A. Якщо його розірвати, більше не залишиться сильних посилань на екземпляр Apartment:

unit4A = nil
// Надрукує "Помешкання 4A деініціалізується"

Оскільки більше не залишилось сильних посилань на екземпляр Apartment, його теж буде деалоковано:


Примітка

У системах з прибиранням сміття, слабкі посиланні іноді використовуються для реалізації простого механізму кешування: там об’єкти, на які немає сильних посилань, деалокуються лише тоді, коли при закінченні пам’яті в системі буде розпочато збірку сміття. Однак, при роботі з ARC, значення деалокуються одразу як тільки вони втрачають останнє сильне посилання, і тому створення слабких посилань не підходить для даної цілі.

Безхазяйні посилання

Як і слабкі посилання, безхазяйні посилання не тримаються міцно за екземпляр, на котрий вони посилаються. Але на відміну від слабких посилань, безхазяйні посилання використовуються тоді, коли час життя іншого екземпляру такий же, як і наш, або довший. Безхазяйні посилання позначаються ключовим словом unowned перед оголошенням змінної чи властивості.

Вважається, що безхазяйне посилання завжди має значення. Як результат, ARC ніколи не присвоює безхазяйному посиланню значення nil, що значить, що безхазяйні посилання можуть мати не опціональний тип.

Важливо

Слід користуватись безхазяйними посиланнями лише при повній впевненості у тому, що вони завжди посилаються на екземпляр, котрий не було деалоковано.

Спроба доступу до значення безхазяйного посилання після деалокації екземпляру призведе до помилки часу виконання.

У наступному прикладі оголошено два класи, Customer та CreditCard, що моделюють клієнта банку та кредитну картку цього клієнта. Обидва ці класи зберігають посилання на екземпляр іншого класу як властивість. Цей зв’язок потенційно може створити цикл сильних посилань.

Зв’язок між екземплярами Customer та CreditCard дещо відрізняється від зв’язку між екземплярами Apartment та Person у вигляді слабкого посилання з прикладу вище. У даній моделі даних, клієнт може мати або не мати кредитну картку, однак кредитна картка є завжди асоційованою з клієнтом. Екземпляр CreditCard ніколи не переживе екземпляр Customer, на який він посилається. Щоб представити це, клас Customer має опціональну властивість card, а клас CreditCard має безхазяйну (і не опціональну) властивість customer.

Навіть більше, новий екземпляр CreditCard може бути створеним лише за допомогою передачі значення номера картки number та екземпляру Customer до ініціалізатора CreditCard. Це гарантує, що екземпляр CreditCard після створення завжди має асоційований екземпляр Customer.

Оскільки кредитна картка завжди має клієнта, з метою уникнення циклу сильних посилань властивість 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 для того, щоб гарантувати, що місткості властивості number достатньо для зберігання 16-значного номера кредитної картки як на 32-бітних, так і на 64-бітних системах.

У наступному фрагменті коду оголошено опціональну змінну типу Customer на ім’я john, котра буде використовуватись для зберігання певного клієнта. Дана змінна має початкове значення nil через свою опціональну природу:

var john: Customer?

Тепер можна створити екземпляр 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:


Оскільки більше нема сильних посилань на екземпляр Customer, його буде деалоковано. Після того, як це відбудеться, не залишиться більше сильних і на екземпляр CreditCard, і його також буде деалоковано:

john = nil
// Надрукує "John Appleseed деініціалізується"
// Надрукує "Картка #1234567890123456 деініціалізується"

Останній фрагмент коду демонструє, що деініціалізатори екземплярів Customer та CreditCard викликаються та друкують їх повідомлення про деініціалізацію як тільки змінній john присвоєно значення nil.

Примітка

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

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

Безхазяйні опціональні посилання

Опціональне посилання на клас можна позначити безхазяйним. У термінах моделі володіння 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 має два безхазяйних посилання, один на відділ, тобто екземпляр класу Department, і друге на наступний курс, який має пройти учень. Клас Course не володіє жодним із цих об’єктів. Кожен курс є частиною певного відділу, тому властивість department не є опціональною. Однак, оскільки не кожен курс має рекомендацію про наступний курс, властивість nextCourse є опціональною.

Ось приклад використання цих класів:

let department = Department(name: "Садівництво")

let intro = Course(name: "Огляд рослин", in: department)
let intermediate = Course(name: "Вирощування звичайних трав", in: department)
let advanced = Course(name: "Догляд за тропічними рослинами", in: department)

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

У коді вище створюється відділ department та три курси (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 вважається повністю проініціалізованим, як тільки його властивості name присвоєно значення всередині ініціалізатора. Це означає, що ініціалізатор класу Country може починати посилатись на властивість self та передавати її далі, як тільки властивості name було задано початкове значення. Таким чином ініціалізатор класу Country може передавати self як один з параметрів ініціалізатора класу City при ініціалізації властивості capitalCity.

Все це означає, що можна створити екземпляри класів Country та City єдиною інструкцією, без створення циклу сильних посилань, і до властивості capitalCity можна звертатись прямо, без необхідності використання знаку оклику для розгортання опціоналу:

var country = Country(name: "Україна", capitalName: "Київ")
print("Столиця країни \(country.name) має назву \(country.capitalCity.name).")
// Надрукує "Столиця країни Україна має назву Київ."

У прикладі вище, використання опціоналів, що розгортаються неявно, дозволяє задовольнити всі вимоги двофазної ініціалізації класу. До властивості capitalCity тепер можна звертатись як і до не опціонального значення після завершення ініціалізації, при цьому уникаючи циклу сильних посилань.

Цикли сильних посилань із замиканнями

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

Цикли сильних посилань можуть також утворюватись, якщо присвоїти замикання властивості екземпляру класу, і якщо тіло цього замикання захоплює даний екземпляр. Це може статись через те, що тіло замикання звертається до властивості екземпляру, наприклад, self.someProperty, або через виклик методу екземпляру, як, наприклад, self.someMethod(). В обох випадках, ці звернення змушують замикання “захопити” self, утворюючи цикл сильних посилань.

Ці цикли сильних посилань утворюють тому, що замикання, як і класи, є типами-посиланнями. Якщо присвоїти замикання властивості, присвоюється посилання на цю властивість. У сутності, це та ж само проблема, що й вище: два сильних посилання тримають одне одного живими. Однак, в цьому випадку не два екземпляри класів, а екземпляр класу та замикання тримають один одного живими.

У Swift є елегантне розв’язання даної проблеми, механізм, що має назву список захоплення замикання. Однак, перед тим, як ознайомитись з тим, як список захоплення замикання допомагає розірвати цикл сильних посилань, варто краще зрозуміти, як такі цикли утворюються.

У прикладі нижче показано, як можна створити цикл сильних посилань внаслідок посилань замикання на 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, котра представляє назву елементу, таку як "h1" для заголовка, "p" для абзацу, чи "br" для розриву рядка. У класі HTMLElement також визначено опціональну властивість text, котрій можна присвоїти рядок, що представляє текст всередині даного елементу HTML.

Окрім цих двох простих властивостей, клас HTMLElement визначає ліниву властивість на ім’я asHTML. Як властивість посилається за замикання, що поєднує значення name та text у рядок із фрагментом HTML. Властивість asHTML має тип () -> String, або “функція, що не приймає параметрів, та повертає значення типу String”.

За замовчанням, властивості asHTML присвоюється замикання, що повертає рядок з текстовим представленням тегу HTML. Цей тег містить опціональне значення text, якщо воно існує, або не містить контенту, якщо text дорівнює nil. Для елементу-абзацу, замикання поверне "<p>якийсь текст</p>" або "<p />", в залежності, яке значення має властивість text, "якийсь текст" чи nil.

Властивість asHTML називається і використовується так, неначе вона є різновидом методу екземпляру. Однак, оскільки asHTML є властивістю-замиканням, а не методом екземпляру, можна замінити її значення за замовчанням іншим замиканням, якщо для конкретного елементу HTML потрібно замінити спосіб його відображення на текст.

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

let heading = HTMLElement(name: "h1")
let defaultText = "якийсь текст за замовчанням"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Надрукує "<h1>якийсь текст за замовчанням</h1>"

Примітка

Властивість asHTML оголошено як ліниву властивість, тому що вона потрібна лише тоді і лише в той момент, коли елемент потрібно відобразити в рядок із HTML. Той факт, що asHTML є лінивою властивістю, дає можливість звертатись до self всередині замикання: до лінивої властивості не можна звертатись до завершення іінціалізації, і до початку існування self.

Клас HTMLElement має єдиний ініціалізатор, котрий приймає аргументи name та (якщо потрібно) text для ініціалізації нового елемента. Цей клас також має деініціалізатор, котрий друкує повідомлення в момент деалокації екземпляру HTMLElement.

Ось так створюється новий екземпляр HTMLElement та друкується його HTML представлення:

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 не буде надруковано, що демонструє, що екземпляр HTMLElementне було деалоковано.

Боротьба з циклами сильних посилань із замиканнями

Розірвати цикл сильних посилань між замиканням та екземпляром класу можна за допомогою списку захоплення в оголошенні замикання. Список захоплення визначає правила захоплення одного чи кількох типів-значень тілом замикання. Як і з циклами сильних посилань між двома екземплярами класів, кожне захоплене посилання оголошується слабким чи безхазяйним замість того, щоб бути сильним. Доречний вибір слабкого чи безхазяйного виду посилання визначається відношеннями між різними частинами коду.

Примітка

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
    // тут йде тіло замикання
}

Слабкі та безхазяйні посилання

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

І навпаки, слід оголошувати захоплення слабким, коли захоплене посилання може набути значення nil в якийсь момент у майбутньому. Слабкі посилання завжди мають опціональний тип, і автоматично набувають значення nil, коли екземпляр, на який вони посилаються, деалоковано. Це дозволяє перевіряти, чи існує екземпляр, в тілі замикання.

Примітка

Якщо захоплене посилання ніколи не набуде значення nil, його завжди слід захоплювати як безхазяйне посилання, а не слабке посилання.

Безхазяйне посилання є доречним способом захоплення для того, щоб розірвати цикл сильних посилань у попередньому прикладі з 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) is being deinitialized")
    }

}

Дана реалізація класу HTMLElement є майже ідентичною до попередньої реалізації: єдиною відмінністю є список захоплення всередині замикання asHTML. В даному випадку, списком замикання є [unowned self], що означає “захопити self як безхазяйне посилання, а не сильне”.

Можна створити екземпляр HTMLElement та надрукувати його HTML представлення, як і раніше:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Надрукує "<p>hello, world</p>"

Ось як виглядають посилання, якщо скористатись списком захоплення:


Цього разу, self захоплюється замиканням як безхазяйне посилання, і тому замикання не тримає екземпляр HTMLElement сильно при захопленні. Якщо прибрати сильне посилання змінної paragraph, присвоївши їй значення nil, екземпляр HTMLElement буде деалоковано, що можна бачити по повідомленню деініціалізатора, котре надрукується в прикладі нижче:

paragraph = nil
// Надрукує "p деініціалізовується"

Детальніше зі списками захоплення можна ознайомитись у підрозділі Списки захоплення.