Автоматичний підрахунок посилань
У 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 деініціалізовується"
Детальніше зі списками захоплення можна ознайомитись у підрозділі Списки захоплення.