Generic (узагальнене програмування) — це механізм Swift, який дозволяє писати типи та функції з параметризованим типом
Головне
Generic (узагальнений тип) — це синтаксична конструкція Swift, яка дозволяє писати гнучкий, багаторазово використовуваний код з параметризацією типів. Замість конкретного типу пишеться замісник у кутових дужках — зазвичай
Без generics довелося б писати окремі функції для кожного типу: swapInts, swapStrings, swapDates. Одна узагальнена функція swap замінює всі три варіанти одним оголошенням. Це зменшує дублювання, підвищує читабельність і знижує ймовірність помилок при копіюванні коду.
func swapValues<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapValues(&x, &y)
Компілятор Swift генерує спеціалізований код для кожного конкретного використання generic. Це означає, що узагальнена функція swapValues
Узагальнені функції — це функції, які приймають один або більше параметрів-типів. Параметри-типи вказуються після імені функції в кутових дужках:
Функція може мати кілька узагальнених параметрів. Наприклад, функція findKey
func findKey<K: Hashable, V>(
for value: V,
in dictionary: [K: V]
) -> K? where V: Equatable {
for (key, dictValue) in dictionary {
if dictValue == value {
return key
}
}
return nil
}
Обмеження K: Hashable та where V: Equatable гарантують, що findKey можна викликати тільки з словником, ключі якого є хешованими, а значення — порівняними. Такі обмеження — не бюрократія, а необхідність: без Hashable неможливо індексувати пошук за ключем, без Equatable неможливо порівняти value з dictValue.
where — це ключове слово Swift, яке накладає додаткові обмеження на узагальнені параметри. На відміну від простого зазначення протоколу в кутових дужках (
Речення where пишеться після сигнатури функції або типу перед відкриттям фігурної дужки. У where можна вказати, що T: Comparable & Hashable (одночасно два протоколи), або що T.U == Int (конкретизація associated type).
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
}
extension Container where Item: Comparable {
func isSorted() -> Bool {
// Імплементація доступна лише якщо Item: Comparable
return true
}
}
Розширення з where додає методи тільки для тих типів, які задовольняють умову. Container
Associated Types (асоційовані типи) — це спосіб зробити протокол узагальненим без вказівки конкретного типу на етапі оголошення. Замість
За допомогою where можна встановлювати зв’язки між associated types різних протоколів. Наприклад, можна вимагати, щоб Item одного протоколу збігався з Iterator.Element іншого. Це гарантує сумісність типів на рівні компіляції.
protocol StackProtocol {
associatedtype Element
mutating func push(_ item: Element)
mutating func pop() -> Element?
}
struct IntStack: StackProtocol {
typealias Element = Int
private var items: [Int] = []
mutating func push(_ item: Int) { items.append(item) }
mutating func pop() -> Int? { items.popLast() }
}
IntStack конкретизує Element як Int за допомогою typealias. Компілятор перевіряє, що всі вимоги протоколу виконуються з цим типом. Без associated types довелося б писати StackProtocol
Узагальнені розширення дозволяють додавати методи до узагальненого типу з додатковими обмеженнями. Це — патерн Б«умовної відповідностіБ», коли тип отримує функціональність тільки за певних умов. Сабскрипти також можуть бути узагальненими.
Сабскрипт може бути узагальненим: він приймає узагальнені параметри в кутових дужках. Це корисно для безпечного доступу до колекцій з довільними індексами, коли індекс може бути різних типів.
extension Array where Element: Numeric {
func sum() -> Element {
return reduce(0, +)
}
}
extension Array {
subscript<Indices: Sequence>(indices: Indices) -> [Element]
where Indices.Element == Int {
return indices.map { self[$0] }
}
}
Така конструкція надає потужну композицію: метод sum з’являється лише в числових масивах, а сабскрипт з довільними індексами працює з будь-якою послідовністю цілочисельних індексів. Компілятор сам керує видимістю цих членів на основі where-умов.
Помилки при використанні generic часто пов’язані з неправильними обмеженнями або спробою використати generic там, де потрібен конкретний тип. Розглянемо три часті сценарії, з якими стикаються розробники.
Додавання узагальнених параметрів там, де достатньо конкретного типу, — це антипатерн. Якщо функція завжди працює з String, не потрібно робити її
Виклик методу на узагальненому параметрі без зазначення, що метод існує, — помилка.
// ❌ Помилка: value не має методу count
func countElements<T>>(value: T) -> Int { value.count }
// ✅ Працює: T: Collection гарантує наявність count
func countElements<T: Collection>(value: T) -> Int { value.count }
Помилка зрозуміла: узагальнений
Часто задавані питання
Generic — це спосіб писати код, який працює з будь-яким типом, не дублюючи одну й ту ж логіку для Int, String та інших типів. Замість конкретного типу використовується замісник
Generic зберігає інформацію про конкретний тип на етапі компіляції, що дозволяє компілятору перевіряти типи та генерувати оптимізований код. Any — це стирання типу: будь-яке значення може бути передане, але тип втрачається і потрібне приведення (as?).
Використовуйте синтаксис T: SomeProtocol в кутових дужках або окреме речення where T: SomeProtocol. Обмеження гарантують, що узагальнений параметр підтримує певні методи та властивості.
Associated Type (associatedtype) — це замісник для типу всередині протоколу. Протокол не вказує конкретний тип, а оголошує associatedtype, який конкретизується в реалізації: Array має Element, Dictionary має Key та Value.
Swift компілює узагальнений код у спеціалізовані версії для кожного конкретного типу. На практиці узагальнена функція для Int не повільніша за рукописну функцію для Int. Накладних витрат на абстракцію немає.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.