Класи та структури
Класи та структури є гнучкими конструктами загального призначення та основними будівельними блоками коду вашої програми. Методи та властивості, що додають функціональність до класів та структур, створюються за допомогою точно такого ж синтаксису, що й функції, константи та змінні.
На відміну від інших мов програмування, Swift не вимагає розділення інтерфейсу та реалізації власного класу на різні файли. У Swift, класи та структури оголошуються в одному файлі, а зовнішній інтерфейс цих класів та структур є автоматично доступним для використання в іншому коді.
Примітка
Екземпляр класу традиційно називається об’єктом. Однак, у Swift класи та структури є ближчими в плані функціональності, ніж в інших мовах, і більша частина цього розділу описуватиме функціональність, що властива екземплярам як класів, так і структур. Тому, найчастіше цей розділ оперуватиме більш загальним терміном “екземпляр”.
Порівняння класів та структур
Класи та структури у Swift мають чимало спільного. Обидва можуть:
- Визначати властивості та зберігати в них значення
- Визначати методи та реалізовувати в них функціональність
- Визначати індекси для доступу до внутрішніх значень за допомогою синтаксису індексації
- Визначати ініціалізатори для налаштування початкового стану
- Бути розширеними для реалізації додаткової функціональності
- Підпорядковуватись протоколам, щоб надавати стандартну функціональність певного виду
За більшою інформацією слід звертатись до розділів Властивості, Методи, Індекси, Ініціалізація, Розширення, та Протоколи.
Класи мають додаткові можливості, яких нема в структур:
- Наслідування дозволяє одному класу наслідувати характеристики іншого.
- Приведення типів дозволяє перевірити та інтерпретувати тип екземпляру класу під час виконання.
- Деініціалізатори дозволяють екземпляру класу звільнити будь-які ресурси, використані під час життєвого циклу
- Підрахунок посилань дозволяє існувати більш ніж одному посиланню на екземпляр класу.
За більшою інформацією слід звертатись до розділів Наслідування, Приведення типів, Деініціалізація, and Автоматичний підрахунок посилань.
Примітка
Структури при передачі по коду завжди копіюються, і тому не послуговуються підрахунком посилань.
Синтаксис оголошення
Класи та структури мають схожий синтаксис оголошення. Оголошення класу розпочинається ключовим словом class
, а оголошення структури - ключовим словом struct
. Оголошення як класу, так і структури розміщується всередині пари фігурних дужок:
class SomeClass {
// тут йде оголошення класу
}
struct SomeStructure {
// тут йде оголошення структури
}
Примітка
Кожне оголошення класу чи структури є оголошенням нового типу Swift. Слід давати класам та структурам назви у стилі ВерхньогоВерблюжогоРегістра (наприклад,
SomeClass
таSomeStructure
), щоб відповідати стилю стандартних типів Swift (таких якString
,Int
, таBool
). В той же час, слід давати властивостям та методам назви у стилі нижньогоВерблюжогоРегістра (наприклад,frameRate
таincrementCount
), щоб відрізняти їх від назв типів.
Ось приклад оголошення структури та оголошення класу:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
У прикладі вище оголошено нову структуру на ім’я Resolution
для опису роздільної здатності екрану в пікселях. Дана структура має дві властивості, що зберігаються, на ім’я width
and height
. Властивості, що зберігаються, є константами чи змінними, що йдуть в комплекті із класом чи структурою, і зберігаються як їх частина. Ці дві властивості мають тип, визначений як Int
через задавання початкового цілочисельного значення 0
.
У прикладі вище також оголошено новий клас на ім’я VideoMode
для опису певного відео режим для екрана. Даний клас має чотири властивості, що зберігаються. Перша властивість, resolution
, ініціалізована новим екземпляром структури Resolution
, через що її тип визначено як Resolution
. Щодо наступних трьох властивостей, нові екземпляри VideoMode
будуть ініціалізовані із налаштуванням interlaced
що дорівнює false
(що означає, що розгортка відео не буде черезрядковою), швидкістю зміни кадрів frameRate
, що дорівнює 0.0
, та опціональним рядком на ім’я name
. Властивість name
буде автоматично ініціалізовано значенням nil
, тобто “нема значення name
”, оскільки вона має опціональний тип.
Екземпляри класів та структур
Оголошення структури Resolution
та класу VideoMode
описують лише те, як буде виглядати роздільна здатність Resolution
та відео режим VideoMode
. Самі по собі вони не описують конкретну роздільну здатність чи відео режим. Для цього слід створити екземпляр структури чи класу.
Синтаксис створення екземплярів структур та класів дуже схожий:
let someResolution = Resolution()
let someVideoMode = VideoMode()
І структури, і класи послуговуються синтаксисом ініціалізації для створення нових екземплярів. У найпростішій формі цього синтаксису, записується назва класу чи структури та порожні круглі дужки після неї, наприклад Resolution()
чи VideoMode()
. Таким чином створюються нові екземпляри класу чи структури, при цьому їх властивості ініціалізуються значенням за замовчанням. Ініціалізація класів та структур детально описана в розділі Ініціалізація.
Доступ до властивостей
Щоб отримати значення властивості екземпляру, використовують синтаксис крапки. При такому синтаксисі, ім’я властивості записується одразу після імені екземпляру, відділяючись від нього крапкою (.
), без жодних пробілів:
print("Ширина в someResolution дорівнює \(someResolution.width)")
// Надрукує "Ширина в someResolution дорівнює 0"
У даному прикладі, someResolution.width
посилається на властивість width
екземпляру someResolution
, і повертає її початкове значення за замовчанням 0
.
Синтаксис крапки можна застосовувати до підвластивостей, таких як властивість width
властивості resolution
класу VideoMode
:
print("Ширина в someVideoMode дорівнює \(someVideoMode.resolution.width)")
// Надрукує "Ширина в someVideoMode дорівнює 0"
// Також можна застосовувати синтаксис крапки для присвоєння нового значення змінній властивості:
someVideoMode.resolution.width = 1280
print("Ширина в someVideoMode тепер дорівнює \(someVideoMode.resolution.width)")
// Надрукує "Ширина в someVideoMode тепер дорівнює 1280"
Примітка
На відміну від Objective-C, мова Swift дозволяє присвоювати значення підвластивостям структур напряму. У останньому прикладі вище, властивість
width
властивостіresolution
екземпляруsomeVideoMode
задається напряму, без необхідності присвоювати повністю нову структуру властивостіresolution
.
Почленна ініціалізація у структурах
Всі структури мають автоматично-згенеровані почленні ініціалізатори, котрі можна використовувати для ініціалізації членів – властивостей нового екземпляру структури. Початкові значення властивостей нового екземпляру можуть передаватись почленному ініціалізатору за іменем:
let vga = Resolution(width: 640, height: 480)
На відміну від структур, екземпляри класів не мають почленних ініціалізаторів за замовчанням. Детальніше з ініціалізаторами можна ознайомитись у розділі Ініціалізація.
Структури та перечислення як типи-значення
Тип-значення – це тип, чиє значення копіюється при присвоєнні до змінної чи константи, або при передачі до функції.
Насправді, типи-значення широко використовувались протягом попередніх розділів цієї книги. Фактично, всі базові типи у Swift: цілі числа, числа з плаваючою комою, булеві значення, рядки, масиви та словники – всі вони є типами-значеннями, і є реалізованими як структури за лаштунками.
Всі структури та перечислення є типами-значеннями у Swift. Це означає, що будь-яка структура чи перечислення, що ви створюєте (та будь-які типи значення, що є їх властивостями) завжди копіюються при їх передачі по коду.
Розглянемо наступний приклад, де використовується структура Resolution
з попереднього прикладу:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
У цьому прикладі оголошується константа hd
, котрій присвоєно екземпляр структури Resolution
, який ініціалізовано шириною та висотою відео у форматі Full HD (1920
пікселі ширини на 1080
пікселі висоти).
Потім оголошено змінну на ім’я cinema
, котрій присвоєно поточне значення hd
. Оскільки Resolution
– це структура, відбувається копіювання існуючого екземпляру, і цю нову копію присвоєно змінній cinema
. Хоч hd
та cinema
зараз мають однакову ширину й висоту, вони являються двома повністю різними екземплярами за лаштунками.
Далі, змінимо властивість width
змінної cinema
, щоб моделювати трошки ширший стандарт 2K, що використовуються в цифровій кінопроекції (2048
пікселі ширини на 1080
пікселі висоти):
cinema.width = 2048
Перевіримо, що властивість width
змінної cinema
дійсно змінилась на 2048
:
print("cinema тепер має \(cinema.width) пікселів ширини")
// Надрукує "cinema тепер має 2048 пікселів ширини"
Однак, властивість width
оригінального екземпляра hd
досі має старе значення 1920
:
print("hd все ще має \(hd.width) пікселів ширини")
// Надрукує "hd все ще має 1920 пікселів ширини"
Коли змінній cinema
було присвоєно поточне значення hd
, значення, що зберігались у hd
, були скопійовані до нового екземпляру cinema
. В кінцевому результаті маємо два повністю розділених екземпляри, котрі лише внаслідок копіювання містять однакові числові значення. Оскільки вони є різними екземплярами, присвоєння ширині cinema
значення 2048
не впливає на значення, що зберігаються в hd
.
Ту ж само поведінку мають перечислення:
enum CompassPoint {
case north, south, east, west
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection = .east
if rememberedDirection == .west {
print("Значення rememberedDirection все ще дорівнює .west")
}
// Надрукує "Значення rememberedDirection все ще дорівнює .west"
Коли константі rememberedDirection
присвоєно значення змінної currentDirection
, їй фактично було присвоєно копію цього значення. Подальші зміни значення currentDirection
не впливають на копію оригінального значення, що зберігається у константі rememberedDirection
.
Класи як типи-посилання
На відміну від типів-значень, типи-посилання не копіюються при присвоєнні їх змінній чи константі, або при передачі до функції. Замість копії використовується посилання на той же само екземпляр.
Ось приклад, де використовуються клас VideoMode
, оголошений вище:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
У даному прикладі оголошено нову константу на ім’я tenEighty
, і їй присвоєно посилання на новий екземпляр класу VideoMode
. Властивості resolution
даного екземпляру присвоєно копію значення hd
з роздільною здатністю 1920
на 1080
із прикладів вище. Далі встановлюються значення черезрядкової розгортки, дається ім’я "1080i"
, а швидкість програвання встановлюється в 25.0
кадрів на секунду.
Далі, присвоїмо tenEighty
новій константі на ім’я alsoTenEighty
, після чого змінимо властивість frameRate
нової константи alsoTenEighty
:
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
Оскільки класи є типами-посиланнями, константи tenEighty
та alsoTenEighty
фактично посилаються на один і той же екземпляр класу VideoMode
. Насправді, вони є просто двома різними назвами одного й того ж єдиного екземпляра.
Перевірка властивості frameRate
екземпляру tenEighty
демонструє, що її значення тепер змінилось і дорівнює 30.0
:
print("Властивість frameRate екземпляру tenEighty тепер дорівнює \(tenEighty.frameRate)")
// Надрукує "Властивість frameRate екземпляру tenEighty тепер дорівнює 30.0"
Зверніть увагу, що tenEighty
та alsoTenEighty
було оголошено константами, а не змінними. Однак, все ще можна змінити tenEighty.frameRate
та alsoTenEighty.frameRate
, оскільки значення самих констант tenEighty
та alsoTenEighty
фактично лишилось без змін. Константи tenEighty
та alsoTenEighty
самі по собі не “зберігають” екземпляр класу VideoMode
: замість цього, вони обидві посилаються на екземпляр класу VideoMode
за лаштунками. Змінилась саме властивість frameRate
екземпляру VideoMode
за лаштунками, а не значення самих константних посилань на цей екземпляр.
Оператори ідентичності
Оскільки класи є типами-посиланнями, можливо мати кілька констант та змінних, що посилається на один і той же екземпляр класу за лаштунками. (Що неможливо для структур та перечислень, оскільки вони завжди копіюються при присвоєнні новій константі чи змінні, чи при передаванні до функції).
Іноді буває потрібно з’ясувати, чи посилаються дві константи або змінні на один і той же екземпляр класу. Для цього у Swift є два оператори ідентичності:
- Ідентичний до (
===
) - Не ідентичний до (
!==
)
Ці оператори застосовують для перевірки, чи посилаються дві константи або змінні на один і той же екземпляр:
if tenEighty === alsoTenEighty {
print("tenEighty та alsoTenEighty посилаються на одий і той же екземпляр VideoMode.")
}
// Надрукує "tenEighty та alsoTenEighty посилаються на одий і той же екземпляр VideoModeи."
Слід зазначити, що “ідентичний до” (записується як три знаки рівності, тобто ===
) означає не те ж саме, що й оператор “дорівнює” (записується як два знаки рівності, тобто ==
):
- “Ідентичний до” означає, що дві константи чи змінні посилаються на один і той же екземпляр класу
- “Дорівнює” означає, що два екземпляри вважаються “рівними”, або “еквівалентними” по значенню, згідно із сенсом рівності, закладеним проєктувальником даного типу.
Коли ви оголошуєте свої власні класи та структури, це саме ваша відповідальність вирішувати, що кваліфікується як рівність двох екземплярів. Процес визначення вашої реалізації операторів “дорівнює” та “не дорівнює” описано в підрозділі Оператори рівності.
Вказівники
Якщо ви маєте досвід із мовами C, C++, чи Objective-C, вам, мабуть, відомо, що в цих мовах для посилань на адреси у пам’яті застосовуються вказівники. У Swift, константи чи змінні, що посилаються на екземпляр якогось типу-посилання є схожим на вказівник у мові C, але вони не є прямими вказівниками на адреси у пам’яті, і не потребують запису зірочки (*
) для позначення створення посилання. У Swift ці посилання визначаються просто як будь-яка інша константа чи змінна.
Вибір між класами та структурами
Для оголошення властних типів даних і використання їх як будівельних блоків коду вашої програми, підходять як класи, так і структури.
Однак, екземпляри структур завжди передаються по значенню, в той час як екземпляри класів завжди передаються за посиланням. Це означає, що вони підходять для різних видів задач. Розглядаючи конструкти даних та функціональність, котрих потребує ваш проєкт, доводиться вирішувати, в якому вигляді краще втілити цей конструкт, у вигляді класу чи структури?
Як основний гайдлайн, слід задуматись над використанням структури, якщо одна чи декілька наступних умов виконуються:
- Основне призначення структур – інкапсулювати кілька відносно простих значень.
- Доцільно очікувати, що інкапсульовані дані будуть копіюватись (а не передаватись за посиланням) при присвоєнні чи передачі до функції екземплярів цієї структури.
- Усі властивості, що зберігаються у структурі, є також типами-значеннями, і очікується, що вони також будуть копіюватись
- Структура не потребує наслідування властивостей чи поведінки існуючого типу.
Ось кілька прикладів хороших кандидатів у структури:
- Розміри геометричної фігури, наприклад, інкапсуляція властивостей ширини та висоти типу
Double
. - Спосіб посилатись на діапазони всередині послідовностей, наприклад, інкапсуляція властивостей початку діапазону та його довжини типу
Int
. - Точка в тривимірній системі координат, наприклад, інкапсуляція властивостей x, y та z типу
Double
.
У всіх інших випадках, слід визначати класи, створювати екземпляри цих класів і передавати їх за посиланням. На практиці, більшість власних типів даних повинні бути класами, а не структурами.
Присвоєння і поведінка копіювання рядків, масивів та словників
У Swift, багато базових типів даних, такі як String
, Array
, та Dictionary
, є реалізовані як структури. Це означає, що дані, такі як рядки, масиви та словники, копіюються при присвоєнні їх новій змінній чи константі, або при передачі до функції чи методу.
Дана поведінка відрізняється від Foundation: NSString
, NSArray
, та NSDictionary
реалізовані як класи, а не структури. Рядки, масиви та словники у Foundation завжди присвоюються та передаються за посиланням на існуючий екземпляр, а не як копії.
Примітка
Текст вище посилаєтсья на “копіювання” рядків, масивів та словників. Ваш код завжди буду вести себе, ніби відбулось копіювання. Однак, у Swift за лаштунками фактичне копіювання відбувається тільки тоді, коли це дійсно потрібно. Swift керує копіюванням усіх значень, щоб гарантувати оптимальну швидкодію коду, тому вам не слід уникати присвоєнь заради оптимізації коду.