1. Дві ролі протоколу: контракт і тип значення
Коли ви вперше чуєте слово protocol, мозок чесно малює одну картинку: «Ага, це ж список вимог». І це правда. Але далі Swift пропонує трюк, який спершу здається магією: той самий протокол можна використовувати як тип змінної.
Важливо не заплутатися: один і той самий символ P може брати участь і в значенні «тип відповідає протоколу», і в значенні «зберігаємо значення за протоколом».
Спершу зафіксуймо дві різні ситуації, які зовні схожі, але за змістом — різні.
protocol PrintableTitle {
var title: String { get }
}
struct Book: PrintableTitle {
let title: String
}
// 1) Відповідність типу протоколу:
let b = Book(title: "Swift для людей")
// 2) Протокол як тип значення (про це лекція):
let x: any PrintableTitle = b
print(x.title) // Swift для людей
У рядку struct Book: PrintableTitle ми говоримо: «тип Book бере на себе обов’язок виконувати контракт».
А в рядку let x: any PrintableTitle = b ми кажемо: «змінна x зберігає значення якогось конкретного типу, але мені потрібні лише можливості PrintableTitle».
У Swift 6 екзистенційні типи, тобто «протокол як тип значення», пишуться явно через any. І це не просто прикраса — це підказка читачеві коду: «обережно, тут тип навмисно стирається до рівня контракту».
2. any P: екзистенціал і «коробка» з невідомим типом
Що таке any P
Уявіть, що ви замовляєте доставку: кур’єр приносить коробку, а всередині може бути що завгодно — книга, чашка, другий монітор «щоб підвищити продуктивність», а насправді — щоб тримати Discord на окремому екрані. Зовні на коробці написано лише одне: «усередині є предмет, який уміє ось ці дії».
any P — це саме така «коробка»: ви знаєте, що всередині лежить значення, яке відповідає P, але який саме це тип, під час написання коду знати не зобов’язані. У документації Swift таку групу типів називають existential types: «існує деякий конкретний тип, який задовольняє контракт».
Важливо: ви не «втрачаєте тип назавжди», а просто свідомо обмежуєте себе. Це дуже схоже на ситуацію, коли ви спілкуєтеся з пристроєм через пульт. Мені не важливо, який саме телевізор стоїть усередині, якщо пульт підтримує кнопки volumeUp() і mute().
Саме тому any P — це інструмент дизайну: ви заздалегідь кажете «я працюю за можливостями».
Чому any у Swift 6 пишеться явно і де його не можна писати
Слово any з’явилося не заради краси. Його додали, щоб ви одразу бачили: тут не «звичайний конкретний тип», а тип-контракт, який може зберігати різні конкретні значення. Це підкреслено в дизайні Swift: екзистенційні типи стали явнішими, щоб їх не плутали з іншими механізмами типізації.
Синтаксис:
let value: any SomeProtocol = ...
Водночас any не можна вживати будь-де. Воно застосовується до протоколів і композицій протоколів, але не має сенсу для конкретних типів. Ось приклад того, чого робити не можна, — і компілятор вас зупинить:
struct S { }
let a: any S = S() // помилка: any не застосовується до конкретного типу
Запам’ятайте просте правило для початківців: any — це маркер «тип стерто до рівня протоколу».
3. Видимий API в any P: доступні лише вимоги протоколу
Найважливіший — і найчесніший — ефект any P такий: щойно ви поклали значення в «коробку», вам стають доступними лише ті члени, які перелічені в протоколі.
Це не баг і не обмеження заради обмеження — це і є сенс дисципліни «працюємо за контрактом».
Давайте розглянемо це на прикладі нашої навчальної CLI-теми «бібліотека книг».
import Foundation
protocol BookLike {
var title: String { get }
}
struct Book: BookLike {
let title: String
let year: Int
}
let item: any BookLike = Book(title: "Алгоритми без сліз", year: 2020)
print(item.title) // Алгоритми без сліз
// print(item.year) // ❌ Помилка компіляції: у BookLike немає year
Чому item.year не працює, хоча всередині справді лежить Book? Тому що змінну оголошено як any BookLike, а контракт BookLike не обіцяв наявності year.
І тут дуже корисно впіймати філософію Swift: компілятор ніби каже вам:
«Ви самі попросили мене забути конкретний тип і залишити лише контракт. Я слухняно забув. Тепер не просіть у мене year, якого в контракті немає».
4. any P у параметрах і колекціях
Функції приймають будь-кого, хто вміє…
Коли ви працюєте самі, можна писати функції, які приймають конкретний Book. Але щойно застосунок зростає, виникає бажання писати універсальніші функції: «мені не важливий конкретний тип, аби в нього був title».
Ось де any P починає сяяти.
import Foundation
protocol Titled {
var title: String { get }
}
func printTitle(_ value: any Titled) {
print("• \(value.title)")
}
struct Book: Titled { let title: String }
struct Magazine: Titled { let title: String }
printTitle(Book(title: "Swift 6.2")) // • Swift 6.2
printTitle(Magazine(title: "iOS Weekly")) // • iOS Weekly
Функція printTitle тепер не прив’язана до Book. Вона приймає будь-який тип, який відповідає Titled. Це і є програмування «за можливостями».
Зверніть увагу на приємний ефект: ви менше залежите від конкретних моделей і більше — від контрактів. Зазвичай це робить код простішим для розширення: без містики, просто з меншою зв’язаністю.
Колекції протокольних значень: [any P]
У реальному застосунку нам часто потрібно зберігати кілька сутностей разом: наприклад, усе, що можна показати користувачеві. Якщо вони мають спільний контракт, ми можемо зібрати їх в один масив.
Важливий момент: масив має зберігати елементи одного типу. Тому якщо ми хочемо змішати Book і Magazine, нам потрібен спільний тип — і ним стає any Titled.
import Foundation
protocol Titled { var title: String { get } }
struct Book: Titled { let title: String }
struct Magazine: Titled { let title: String }
let items: [any Titled] = [
Book(title: "Практика Swift"),
Magazine(title: "Dev Digest")
]
for i in items {
print(i.title)
}
// Практика Swift
// Dev Digest
Тут «коробка» (any Titled) дозволяє скласти в один масив різні конкретні типи, доки вони виконують контракт.
Якщо ви любите схеми, ось дуже спрощена картинка:
flowchart LR
B[Книга] --> P[any Titled]
M[Журнал] --> P
P --> A[[Масив any Titled]]
Сенс такий: конкретні типи піднімаються до рівня контракту і стають сумісними в одній колекції.
5. Повернення до конкретики: безпечний downcast через as?
Іноді справді потрібно дістатися до деталей конкретного типу. Наприклад, у масиві [any Titled] ви хочете друкувати рік лише для книг.
Це нормально, але робиться усвідомлено і безпечно: через as? (а не через «я впевнений, мамою клянусь» у вигляді as!).
import Foundation
protocol Titled { var title: String { get } }
struct Book: Titled {
let title: String
let year: Int
}
struct Magazine: Titled { let title: String }
let items: [any Titled] = [
Book(title: "Swift", year: 2024),
Magazine(title: "Weekly")
]
for i in items {
if let b = i as? Book {
print("\(b.title) (\(b.year))") // Swift (2024)
} else {
print(i.title) // Weekly
}
}
Тут спрацьовує одразу кілька тем: as? повертає Optional, тому ми використовуємо if let. Це акуратний, «свіфтовий» спосіб сказати: «якщо всередині коробки опинився Book — використай деталі, інакше працюй за контрактом».
Чому не as!? Тому що as! — це обіцянка компілятору, що тип точно збіжиться. Якщо не збіжиться, буде runtime-круш. А ваш CLI-застосунок і ваша психіка не зобов’язані страждати.
6. any дисциплінує дизайн: протоколи мають бути маленькими
Є спокуса: раз через any P видно лише вимоги протоколу, давайте додамо в протокол узагалі все, що нам колись може знадобитися. Вийде «суперпротокол» на 25 вимог, який уміє і друкувати, і валідувати, і літати, і варити каву.
Проблема в тому, що такий протокол починає погано виконувати роль «контракту можливостей». Контракт корисний тоді, коли він мінімальний і зрозумілий: «щоб показати елемент користувачеві, потрібен title», «щоб зберегти — потрібен toCSVLine()», «щоб порахувати — потрібен price».
Чим менше обов’язків ви запихаєте в один контракт, тим простіше потім збирати поведінку з кількох контрактів. Композицію P & Q ми обговорюватимемо в наступних лекціях курсу, а сьогодні просто тримайте в голові ідею «не роздувати».
Практичний критерій для початківця такий: якщо ви не можете пояснити протокол однією фразою — «будь-хто, хто вміє …» — значить, протокол, найімовірніше, став занадто товстим.
7. Приклад у CLI: різні формати виведення книг через any
Тепер прив’яжімо any P до нашого навчального застосунку — CLI-бібліотеки. На цьому етапі курсу у нас уже є моделі (struct Book), команди й вивід у консоль.
Ми не будемо чіпати майбутні архітектурні шари та модулі — просто зробимо маленький, але дуже життєвий крок: виведення книг у різних форматах без прив’язки основного коду до конкретного форматування.
Протокол «рендерер книг» і дві реалізації
Спочатку описуємо контракт. Він чесний і маленький: «вміє перетворити список книг на рядок».
import Foundation
struct Book {
let title: String
let year: Int
}
protocol BookRendering {
func render(_ books: [Book]) -> String
}
Тепер — дві реалізації: «звичайний текст» і «майже CSV» (без фанатизму).
import Foundation
struct PlainTextBookRenderer: BookRendering {
func render(_ books: [Book]) -> String {
books.map { "\($0.title) (\($0.year))" }.joined(separator: "\n")
}
}
struct CSVBookRenderer: BookRendering {
func render(_ books: [Book]) -> String {
books.map { "\($0.title);\($0.year)" }.joined(separator: "\n")
}
}
Зверніть увагу: рендерери — окремі типи. Book нічого не знає про формати. Це зручно.
Змінна «поточний рендерер» як any BookRendering
Тепер головне: ми хочемо вибрати формат виведення за умовою, але зберігати вибране значення в одній змінній. Отже, нам потрібен спільний тип — і ним стає any BookRendering.
import Foundation
let books = [
Book(title: "Swift", year: 2024),
Book(title: "CLI Design", year: 2023)
]
let useCSV = true
let renderer: any BookRendering = useCSV
? CSVBookRenderer()
: PlainTextBookRenderer()
print(renderer.render(books))
// Swift;2024
// CLI Design;2023
Тут renderer — це «коробка з контрактом»: усередині або CSVBookRenderer, або PlainTextBookRenderer, але нам байдуже — ми викликаємо render, тому що це частина протоколу.
Важлива думка: ми отримали перемикальну поведінку без зміни решти коду. І це без успадкування, без «божественних класів», без магії.
Функція, яка працює лише за контрактом
Щоб закріпити ідею, винесімо друк в окрему функцію: вона приймає any BookRendering, отже їй не потрібні конкретні типи форматерів.
import Foundation
func printBooks(_ books: [Book], using renderer: any BookRendering) {
let text = renderer.render(books)
print(text)
}
printBooks(books, using: PlainTextBookRenderer())
// Swift (2024)
// CLI Design (2023)
Це стиль «за можливостями» у чистому вигляді: функція приймає «кого завгодно, хто вміє рендерити книги».
Мінісхема потоку даних
Корисно тримати в голові, де саме any допомагає:
flowchart TD
A[Список Book] --> B[renderer: any BookRendering]
B --> C["render(books) -> String"]
C --> D["print()"]
Тут any BookRendering — точка, де ми свідомо стираємо конкретний тип форматера і працюємо лише за контрактом.
8. Типові помилки під час роботи з any P
Помилка №1: намагатися викликати методи конкретного типу через any P.
Дуже часта ситуація: ви поклали Book у any Titled, а потім дивуєтеся, чому не бачите year. Це не «Swift вередує», це ви самі попросили працювати за контрактом. Рішення зазвичай одне з двох: або додати вимогу в протокол, якщо це справді частина контракту, або зробити безпечний downcast через as? там, де потрібна конкретика.
Помилка №2: використовувати as! замість as? «тому що я впевнений».
as! — це швидкий шлях до падіння застосунку під час виконання. Особливо неприємно це в CLI, де користувач отримує не зрозуміле повідомлення про помилку, а просто crash. Якщо вам здається, що «точно завжди буде Book», краще або змінити дизайн так, щоб тип не втрачався, або хоча б зробити as? і обробити else нормальним повідомленням.
Помилка №3: роздувати протокол до розмірів «суперінтерфейсу».
Коли ви починаєте активно використовувати any, з’являється бажання зробити протокол таким, щоб «через коробку було доступне все». Зазвичай це закінчується контрактом на 15 вимог, який ніхто не може реалізувати без болю. Хороший протокол легко вимовити вголос однією фразою. Якщо це не виходить — імовірно, контракт треба розділити.
Помилка №4: плутати «тип відповідає протоколу» і «значення зберігається як протокол».
struct S: P — це обіцянка компілятору про тип.
let x: any P = ... — це вибір подати значення через контракт.
Якщо ці дві ролі змішати в голові, починаються дивні запитання на кшталт «чому протокол не зберігає дані?» або «чому змінна не знає реальний тип?». Протокол не зберігає дані; він описує API. А any P навмисно приховує конкретику.
Помилка №5: забувати, що any — це свідоме обмеження.
Іноді початківці пишуть any «тому що так сказали», а потім увесь час змушені робити as? назад. Це сигнал, що на цій ділянці коду вам, можливо, потрібен конкретний тип, а не екзистенційний. any корисний там, де вам справді важливі лише можливості, а не деталі реалізації.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ