1. Навіщо потрібні члени типу
Коли ви тільки починаєте писати класи, легко звикнути до думки: «Якщо це клас, то я створюю об’єкт і викликаю методи на ньому». Але в реальних програмах функціональність часто потрібна ще до створення екземпляра: константи за замовчуванням, фабричні методи, лічильники створених об’єктів, короткі утиліти для форматування. І хочеться, щоб усе це природно було пов’язане з типом, а не висіло окремою глобальною функцією.
У Swift для цього є два рівні API: члени екземпляра — те, що належить конкретному об’єкту, і члени типу — те, що належить типу загалом. Виклик виду user.rename() — це метод екземпляра: він потребує об’єкта. А виклик виду User.makeAnonymous() — це член типу: його викликають через імʼя типу, і він не потребує попередньо створеного об’єкта.
Важливо: члени типу — це не «якась магія з майбутніх тем». Це просто акуратний спосіб сказати: «Це стосується всього класу, а не одного екземпляра». І саме тут з’являється цікава розвилка: одні речі ми хочемо зробити неперевизначуваними, а для інших — навпаки, залишити можливість підлаштувати поведінку в похідному типі.
2. static members: фіксована поведінка
Якщо ви ставите static у властивості або методі всередині класу, ви ніби кажете компілятору: «Це належить типу, і я не очікую, що хтось змінюватиме цю поведінку через успадкування». Можна подумки перекладати static як «намертво прив’язане до цього типу».
На практиці static найчастіше використовують для двох речей. По-перше, це константи й типові значення: наприклад, «скільки книжок показуємо на сторінці», «який роздільник використовуємо у виведенні», «який формат дати вважаємо стандартним». По-друге, це фабричні методи — зручні способи створити екземпляр із попередніми налаштуваннями.
Поки що це звучить майже так само, як class func / class var. Але різниця, заради якої ми тут зібралися, така: static у класі не призначений для перевизначення. Навіть якщо пізніше з’явиться підклас, він не зможе замінити реалізацію static-методу.
Це корисне обмеження: іноді вам важливо знати, що логіка «за замовчуванням» завжди однакова і без сюрпризів. Програмування взагалі складається з двох частин: написати код, а потім не плакати, коли він починає жити власним життям. static допомагає саме з другою.
import Foundation
final class LibraryConfig {
static let maxFeaturedBooks: Int = 3
static func bannerTitle() -> String {
"Ласкаво просимо до нашої мінібібліотеки!"
}
}
print(LibraryConfig.maxFeaturedBooks) // 3
print(LibraryConfig.bannerTitle()) // Ласкаво просимо до нашої мінібібліотеки!
Тут LibraryConfig взагалі не потрібно створювати як об’єкт. Це «коробка» для налаштувань на рівні типу.
3. class members: допускають перевизначення
Ключове слово class у Swift багатозначне, як слово “bank” англійською: воно може означати «оголошення класу» (class User { ... }), а може означати «член типу, який можна перевизначити» (class func ... / class var ...). І саме друге значення нам потрібне в цій лекції.
Коли ви оголошуєте член типу з ключовим словом class, ви ніби залишаєте двері прочиненими: якщо хтось створить підклас, то зможе визначити власну версію цього методу чи властивості. Це схоже на побутову ситуацію: «Я готую борщ за цим рецептом, але якщо хтось у родині захоче додати квасолю — будь ласка». Із static квасолю додавати не можна: рецепт уже запаяний.
Важливо: ми не заглиблюємося в механіку успадкування та override — це окрема велика тема. Але саму різницю варто відчути вже зараз: class member — це «на рівні типу, але з можливістю бути іншим у підкласів».
Ще один практичний нюанс: class var у Swift — це зазвичай computed property, тобто властивість із тілом { ... }. Зберігати stored property як class var не можна: зберігати вміє static, а class частіше використовують саме для обчислюваних значень або функцій.
import Foundation
class ReportFormatter {
class func header() -> String { "=== ЗВІТ ===" }
}
class DebugReportFormatter: ReportFormatter {
override class func header() -> String { "=== DEBUG ЗВІТ ===" }
}
print(ReportFormatter.header()) // === ЗВІТ ===
print(DebugReportFormatter.header()) // === DEBUG ЗВІТ ===
Тут важливо не стільки запам’ятати синтаксис override — він ще закріплюватиметься, — скільки побачити ідею: class func залишає можливість іншої реалізації в «споріднених типах».
Порівняння static і class
Коли ключові слова схожі, мозок починає економити енергію і каже: «Та яка різниця, обидва ж “на рівні типу”». Але різниця є, і вона саме про перевизначуваність. Щоб було легше тримати це в голові, зведімо все в таблицю.
Після короткого знайомства видно, що static — це «фіксоване», а class — це «гнучке». І це не про «добре/погано», а про те, який контракт ви хочете задати.
| Що порівнюємо | static member у класі | class member у класі |
|---|---|---|
| Викликається як | |
|
| Можна перевизначати в підкласі | ні (ідея: «прив’язано до типу») | так (ідея: «можна підлаштувати») |
| Підходить для stored properties | так (static let/var) | ні (зазвичай лише computed class var) |
| Підходить для фабрик і утиліт | так | так |
| Типовий сенс | «єдине правило для всіх» | «загальна ідея, але допускаємо варіанти» |
Тут корисно запам’ятати одне майже побутове правило. Якщо значення має бути однаковим для всіх і ви не хочете, щоб його змінювали через успадкування, беріть static. Якщо ви наперед розумієте, що в різних варіантах класу логіка може відрізнятися і це нормальна частина дизайну, беріть class.
І ще одна деталь, яка допомагає не плутатися: static трапляється і в struct, і в enum, і в class. А class як модифікатор члена типу трапляється лише всередині класів, бо саме в класів є успадкування як механізм.
4. Практика: звіт мінібібліотеки
Щоб знання не вилетіли з голови одразу після Run, давайте вплетемо static і class members у маленький шматочок нашого навчального консольного застосунку «мінібібліотека». Ми не будуємо повноцінну систему — нам важливо відчути, де це зручно.
Уявімо, що в нас є сутність книги — поки що максимально проста, — і ми хочемо друкувати «звіт»: список книг із заголовком. Заголовок у звичайному режимі один, а у відладному — інший. Саме тут class func виглядає природно.
import Foundation
struct Book {
let id: Int
let title: String
}
class LibraryReport {
class func header() -> String { "Перелік книг" }
static func printBooks(_ books: [Book]) {
print(header())
for b in books { print("#\(b.id): \(b.title)") }
}
}
Зверніть увагу на зв’язок: метод виведення звіту ми зробили static, а заголовок — class. Чому так? Тому що сама процедура «виведи заголовок і рядки» в нас однакова, і ми не хочемо, щоб її переписували заново. А от заголовок — річ, яку логічно змінювати в різних версіях звіту.
Тепер додамо відладну версію звіту, яка змінює лише заголовок. Ідея проста: виведення лишається тим самим, але заголовок інший.
import Foundation
class DebugLibraryReport: LibraryReport {
override class func header() -> String { "Перелік книг (DEBUG)" }
}
let books = [Book(id: 1, title: "Dune"), Book(id: 2, title: "Solaris")]
LibraryReport.printBooks(books)
// Перелік книг
// #1: Dune
// #2: Solaris
DebugLibraryReport.printBooks(books)
// Перелік книг (DEBUG)
// #1: Dune
// #2: Solaris
Якщо вам зараз здається, що це забагато лише заради заголовка, — ви маєте рацію, і це нормально. Ми навчаємося на маленькому прикладі, щоб потім не будувати величезну систему з глобальних констант і випадкових функцій.
5. TypeName.member vs Self.member
Коли ви всередині класу звертаєтеся до члена типу, ви можете написати або LibraryReport.header(), або Self.header(). На старті здається, що це просто два способи записати одне й те саме. У простих випадках — так. Але щойно з’являється ідея «може бути перевизначено», Self починає звучати інакше.
Інтуїтивно TypeName.something — це пряме посилання на конкретний тип. А Self.something — це «те саме, але відносно поточного типу». Саме це «відносно поточного типу» дає змогу логіці коректно працювати, якщо class member перевизначено.
Іноді це пояснюють так: у контексті класу Self означає динамічний тип self. Тому звернення до class members через динамічний тип і через імʼя типу може поводитися по-різному, якщо члени не final.
Давайте перепишемо виведення звіту так, щоб воно викликало Self.header(). Тоді механізм виведення залишається один, але він коректно підхоплює заголовок похідного типу.
import Foundation
class LibraryReport2 {
class func header() -> String { "Перелік книг" }
static func printHeader() {
print(Self.header())
}
}
class DebugLibraryReport2: LibraryReport2 {
override class func header() -> String { "Перелік книг (DEBUG)" }
}
LibraryReport2.printHeader() // Перелік книг
DebugLibraryReport2.printHeader() // Перелік книг (DEBUG)
Тут ключовий момент не в тому, що «треба завжди писати Self». А в тому, що якщо ви свідомо обрали class — тобто допускаєте перевизначення, — то й викликати такий member часто логічніше через Self, щоб поважати цю ідею.
Як обирати між static і class
Коли ви починаєте проєктувати API класу, ви фактично пишете мінідоговір із майбутнім собою, а можливо, і з іншими розробниками. static і class — це не просто синтаксис, а спосіб висловити намір: «це фіксовано» або «це допускає варіації». І дуже корисно обирати не «як звичніше», а «який контракт я хочу».
Якщо ви робите static let defaultValue, ви кажете: «Це значення — частина визначення типу. Воно не повинно раптово ставати іншим у підкласів». Це зручно для констант, форматних рядків, маленьких утиліт, фабрик із жорсткою поведінкою.
Якщо ви робите class func makeDefault() або class var header: String { ... }, ви залишаєте простір для розширення поведінки через успадкування. Це буває корисно, коли клас явно задумано як база для кількох варіантів поведінки. Навіть якщо ви прямо зараз не пишете підкласи, ви можете заздалегідь закласти таку точку розширення.
При цьому є пастка: новачку легко почати робити все class, бо «раптом знадобиться». Чим більше «можливостей для розширення», тим більше несподіваних комбінацій поведінки ви допускаєте. Тому за замовчуванням спокійніше починати зі static, а class вводити там, де ви справді розумієте сценарій варіативності.
6. Типові помилки під час роботи з static і class members
Помилка № 1: плутати static і class як «два способи записати одне й те саме».
Спочатку це справді може так здаватися: і там, і там виклик через TypeName.member. Але сенс різний: static — «фіксована поведінка», class — «може бути перевизначено». Якщо ви не розрізняєте ці наміри, то або випадково забороняєте потрібну варіативність, або дозволяєте її там, де вона вам не потрібна.
Помилка № 2: намагатися перевизначити static member і дивуватися, чому компілятор проти.
Це класична ситуація: ви зробили static func header(), потім вирішили, що в «особливій версії» потрібно змінити заголовок, і написали override. Компілятор справедливо відповідає: «Не можна». І це не шкідливість Swift, а наслідок вашого ж контракту: static саме й означає, що перевизначення не передбачено.
Приклад помилки компіляції:
class A {
static func f() { }
}
class B: A {
override static func f() { } // ❌ так не можна
}
Помилка № 3: використовувати TypeName.member там, де ви хотіли підтримати варіативність, і тим самим жорстко зафіксувати поведінку.
Якщо ви задумали class func header(), але всередині логіки виведення написали LibraryReport.header(), ви фактично сказали: «Мені байдуже на перевизначення». Потім ви створите підклас, а заголовок не зміниться — і це виглядатиме як містична помилка, хоча проблема просто в тому, що виклик прив’язано до базового типу. У таких місцях зазвичай логічніше Self.header().
Помилка № 4: перетворювати static var на глобальний змінюваний стан без причини.
static var у класі — це одне значення на весь тип. Це потужно, але небезпечно: якщо багато місць у програмі змінюватимуть цей static var, поведінка стане важкою для передбачення (у стилі «чому сьогодні воно друкує 3 книжки, а вчора друкувало 5?»). На цьому етапі курсу достатньо запам’ятати просте правило: починайте зі static let, а до static var переходьте лише тоді, коли справді потрібен змінюваний стан на рівні типу.
Помилка № 5: плутати class в оголошенні типу і class в оголошенні члена типу.
class User { ... } — це «оголосили клас». class func make() — це «оголосили член типу, який допускає перевизначення». Це різні значення одного й того самого слова. Якщо вони змішуються в голові, код починає здаватися страшнішим, ніж є насправді. Зазвичай допомагає звичка вголос читати class func як «перевизначуваний метод типу», а static func — як «фіксований метод типу».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ