Узагальнення
Узагальнений код дозволяє писати гнучкі функції та типи, котрі можна повторно використовувати, що працюють із будь-якими типами відповідно до ваших вимог. Таким способом можна писати код, що уникає повторень, та виражає намір автора у зрозумілій та абстрагованій манері.
Узагальнення є однією з найбільш потужних можливостей мови Swift, і велика частина стандартної бібліотеки Swift побудована за допомогою узагальненого коду. Фактично, ми користувались узагальненнями всюди у Керівництві з мови, навіть якщо ми не усвідомлювали цього. Наприклад, типи Array
та Dictionary
у Swift є узагальненими колекціями. Можна створити масив, що зберігає значення типу Int
, або типу String
, або будь-якого іншого типу, що можна створити у Swift. Аналогічно, можна створити словник, що зберігає значення будь-якого вказаного типу, і немає жодних обмежень щодо типу цих значень.
Проблема, котру вирішують узагальнення
Ось стандартна, не узагальнена функція, що називається swapTwoInts(_:_:)
, та міняє місцями два значення типу Int
:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
Ця функція для обміну значень a
та b
використовує двонаправлені параметри, що описані у підрозділі Двонаправлені параметри.
Функція swapTwoInts(_:_:)
підставляє початкове значення b
у змінну a
, і початкове значення a
у змінну b
. Можна викликати цю функцію для обміну двох значень типу Int
:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt тепер дорівнює \(someInt), а anotherInt тепер дорівнює \(anotherInt)")
// Надрукує "someInt тепер дорівнює 107, а anotherInt тепер дорівнює 3"
Функція swapTwoInts(_:_:)
є корисною, але її можна застосувати лише до пари значень типу Int
. Якщо потрібно обміняти значеннями дві змінні типу String
чи Double
, потрібно написати ще функцій, наприклад swapTwoStrings(_:_:)
та swapTwoDoubles(_:_:)
:
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
Слід помітити, що тіла функцій swapTwoInts(_:_:)
, swapTwoStrings(_:_:)
, та swapTwoDoubles(_:_:)
повністю співпадають. Єдиною відмінністю є тип значень, що приймають ці функції (Int
, String
, та Double
).
Було б ефективніше та набагато більш гнучко написати одну функцію, що обмінює значеннями два значення будь-якого типу. Узагальнений код дозволяє написати таку функцію. (Узагальнена версія цих трьох функцій оголошується далі).
Примітка
У всіх цих трьох функціях, тип у змінних
a
таb
має бути однаковим. Якщо тип уa
таb
не співпадає, неможливо обміняти їх значеннями. Swift є типобезпечною мовою, і тому не дозволяє (наприклад) обміняти значеннями змінні типуString
таDouble
. Спроба зробити це призводить до помилки часу компіляції.
Узагальнені функції
Узагальнені функції можуть працювати з будь-яким типом. Ось приклад узагальненої версії функції swapTwoInts(_:_:)
з прикладу вище, котра називається swapTwoValues(_:_:)
:
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
Тіло функції swapTwoValues(_:_:)
є ідентичним до тіла функції swapTwoInts(_:_:)
. Однак, перший рядок функції swapTwoValues(_:_:)
трохи відрізняється від swapTwoInts(_:_:)
. Ось перші два рядки в порівнянні:
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
Узагальнена функція використовує замісник назви типу (що в даному випадку називається T
) замість фактичної назви типу (на кшталт Int
, String
, чи Double
). Замісник назви типу нічого не каже про то, чим може бути T
, але він каже, що a
та b
повинні мати однаковий тип T
, яким би він не був. Фактичний тип для використання замість T
встановлюється в момент виклику функції swapTwoValues(_:_:)
.
Різниця між узагальненою та не узагальненою функцією полягає також у тому, що після назви узагальненої функції (swapTwoValues(_:_:)
) йде замісник назви типу (T
) у фігурних дужках (<T>
). Фігурні дужки підказують компілятору Swift, що T
– це замісник назви типу у визначенні функції swapTwoValues(_:_:)
. Оскільки T
– це замісник назви типу, компілятор Swift не шукатиме фактичний тип, що називається T
.
Функція swapTwoValues(_:_:)
може тепер викликатись так само, як і swapTwoInts(_:_:)
, але їй можна передавати пару значень будь-якого типу, головне, щоб це були значення одного й того ж типу. При кожному виклику swapTwoValues(_:_:)
, тип, який слід використовувати замість T
, визначається з типів значень, що передаються до функції.
У двох прикладах нижче, T
визначається як Int
та String
відповідно:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt тепер дорівнює 107, а anotherInt тепер дорівнює 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString тепер дорівнює "world", а anotherString тепер дорівнює "hello"
Примітка
Визначена вище функція
swapTwoValues(_:_:)
є аналогом узагальненої функціїswap(::)
, що є частиною стандартної бібліотеки Swift, та є автоматично доступною для використання у ваших додатках. Якщо вам потрібна поведінка функціїswapTwoValues(_:_:)
у вашому коді, слід використовувати існуючу функціюswap(::)
замість того, щоб реалізовувати її самотужки.
Параметри типів
У прикладі з swapTwoValues(_:_:)
, замісник назви типу T
є прикладом параметра типу. Параметри типів визначають та іменують замісник типу, і записуються одразу після назви функції, всередині пари кутових дужок (наприклад, <T>
).
Як тільки вказано параметр типу, його можна використовувати для визначення типу параметрів функції (наприклад, типи параметрів a
та b
функції swapTwoValues(_:_:)
), або типу, що повертається функцією, або як анотацію типу всередині тіла функції. В кожному випадку, тип параметра замінюється фактичним типом при кожному виклику функції. (У прикладі з swapTwoValues(_:_:)
вище, T
замінювався на Int
у першому виклику функції, та на String
у другому виклику функції).
Функція може мати декілька параметрів типів, їх так само записують у кутових дужках, розділюючи комами.
Іменування параметрів типів
У більшості випадків, параметри типів мають змістовні назви, як наприклад Key
(ключ) та Value
(значення) у Dictionary<Key, Value>
, котрі вказують читачу на зв’язок між параметром типу та узагальненим типом або функцією, в якому він використовується. Однак, у випадках, коли між ними нема змістовного зв’язку, традиційно типи параметрів однією великою літерою, зазвичай T
, U
, чи V
, як T
у функції swapTwoValues(_:_:)
вище.
Примітка
Слід завжди давати параметрам типів назви, що пишуться ВерхнімВерблюжимРегістром (наприклад,
T
абоMyTypeParameter
), щоб позначити, що це замісник типу, а не значення.
Узагальнені типи
Окрім узагальнених функцій, Swift дає можливість створювати власні узагальнені типи. Ними можуть бути власні класи, структури та перечислення, що можуть працювати з будь-яким типом, аналогічно до вбудованих Array
та Dictionary
.
Цей підрозділ покаже, як написати власну узагальнену колекцію на ім’я Stack
. Стек - це впорядкована множина значень, аналогічна до масиву, але із більш обмеженим набором операцій, ніж у масивів у Swift. Масиви у Swift дозволяють вставляти чи видаляти елементи в будь-якій позиції. Стек дозволяють новим елементам додаватись лише в кінець колекції (про що кажуть: “заштовхнути елемент у стек”, або push). Стек також дозволяє елементам видалятись, але лише з кінця колекції (про що кажуть: “виштовхнути елемент зі стека”, або pop).
Примітка
Концепція стеку використовується класом
UINavigationController
для моделювання в’ю-контоллерів у ієрархії навігації в додатках на iOS. Щоб додати в’ю-контроллер до навігаційного стеку, слід викликати методpushViewController(_:animated:)
класуUINavigationController
, а щоб прибрати (тобто виштовхнути) в’ю-контроллер з навігаційного стеку – методpopViewControllerAnimated(_:)
. Стек є корисною моделлю колекції в тих випадках, де для керування колекцією потрібне строге правило “останнім прийшов — першим пішов” (або LIFO, від англійсього “last in, first out”).
Ілюстрація нижче демонструє поведінку стека і його операції push та pop:
- У стеку зберігаються три значення.
- До стека заштовхується четверте значення.
- Стек тепер тримає чотири значення, при цьому останнє лежить на його вершині.
- Зі стека виштовхується значення.
- Після виштовхування значення, стек знову містить три початкові значення.
Ось як можна записати не узагальнену версію стеку, в даному випадку для стека значень типу Int
:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
У даній структурі для зберігання значень у стеку використовується масив значень типу Int
, що зберігається у властивості на ім’я items
. Цей стек має два методи, push()
та pop()
, що відповідно заштовхують та виштовхують елементи стеку. Ці методи позначені модифікатором mutating
, оскільки їм потрібно змінити масив, що зберігається у даній структурі.
Однак тип IntStack
може використовуватись лише для зберігання значень типу Int
. Було б набагато більш ефективно створити узагальнений клас Stack
, котрий би керував стеком значень будь-якого типу.
Ось узагальнена версія того ж коду:
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
Слід помітити, що узагальнена версія Stack
є практично такою ж, як і не узагальнена версія, єдиною відмінністю є параметр типу на ім’я Element
замість фактичного типу Int
. Параметр типу записується всередині пари кутових дужок (<Element>
) одразу після назви структури.
Element
визначає замісник назви типу, що буде надана пізніше. До цього майбутнього типу далі можна звертатись як Element
будь-де у визначенні структури. В даному випадку, Element
використовується як замісник у трьох місцях:
- Для створення властивості
items
, котра ініціалізується порожнім масивом значень типуElement
. - Щоб вказати, що метод
push(_:)
має єдиний параметр на ім’яitem
, котрий повинен мати типElement
. - Щоб вказати, що метод
pop()
повертає значення типуElement
.
Оскільки Stack
є узагальненим типом, його можна використовувати для створення стеку будь-якого типу Swift, аналогічно до Array
та Dictionary
.
Щоб створити новий екземпляр Stack
, слід записати тип значень, що зберігатиметься у стеку, у фігурних дужках. Наприклад, щоб створити стек рядків, слід написати Stack<String>()
:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// стек містить 4 рядки
Ось як виглядає стек stackOfStrings
після заштовхування в нього чотирьох значень:
Виштовхування значень зі стека видаляє та повертає значення на вершині стеку, "cuatro"
:
let fromTheTop = stackOfStrings.pop()
// fromTheTop тепер дорівнює "cuatro", а стек тепер містить 3 рядки
Ось як виглядає стек після виштовхування верхнього значення:
Розширення узагальнених типів
При розширенні узагальненого типу не потрібно вказувати список параметрів типів у оголошенні розширення. При цьому список параметрів типів з оригінального оголошення типу є доступним для тіла розширення, і оригінальні назви параметрів типів можна використовувати для посилання на параметри в оригінальному оголошенні.
Наступний приклад розширює узагальнений тип Stack
, додаючи до нього властивість тільки для читання, що обчислюється і називається topItem
, котра повертає елемент на вершині стеку, без виштовхування цього елементу зі стека:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
Властивість topItem
повертає опціональне значення типу Element
. Якщо стер порожній, topItem
поверне nil
; якщо стек не порожній, topItem
поверне останній елемент у масиві items
.
Варто помітити, що розширення не оголошує список параметрів типів. При цьому назва існуючого параметра типу Element
використовується всередині розширення для позначення опціонального типу властивості topItem
.
Властивість, що обчислюється, topItem
тепер може бути використаною з будь-яким екземпляром Stack
для отримання елемента на вершині стеку без його видалення:
if let topItem = stackOfStrings.topItem {
print("Елементом на вершині стеку є '\(topItem)'.")
}
// Надрукує "Елементом на вершині стеку є 'tres'."
Розширення узагальненого типу можуть також включати вимоги, котрі мають задовольняти типи, що отримають нову функціональність. Це детальніше описано далі, в підрозділі Розширення з інструкцією узагальнення Where.
Обмеження типів
Функція swapTwoValues(_:_:)
та тип Stack
можуть працювати із будь-яким типом. Однак, часто буває потрібно накласти певні обмеження на типи, які можуть використовуватись в узагальнених функціях та типах. Обмеження типів вказують, що параметр типу повинен наслідуватись від певного класу, або підпорядковуватись певному протоколу, чи композиції протоколів.
Наприклад, тип Dictionary
у Swift вводить обмеження на типи, що можуть використовуватись як ключі у словнику. Як описано у підрозділі Словники, тип ключа у словнику має підпорядковуватись протоколу Hashable
, тобто вміти обчислити свій хеш. Іншими словами, ключ повинен надавати своє однозначне представлення. Словнику потрібно знаходити хеш ключа для того, щоб перевіряти, чи містить він для цього ключа значення. Без цього обмеження, словник не мав би змоги з’ясувати, потрібно вставляти нове значення чи заміняти існуючи, а також не мав би змоги з’ясувати, чи міститься у ньому значення за заданим ключем.
Дана вимога ставиться за допомогою обмеження типу ключа для Dictionary
, котра визначає, що ключ має підпорядковуватись протоколу Hashable
, спеціальному протоколу зі стандартної бібліотеки Swift. Всі базові типи Swift (такі, як String
, Int
, Double
, та Bool
) є за замовчанням підпорядкованими цьому протоколу.
Створюючи власні узагальнені типи, можна ставити власні обмеження типів, і ці обмеження містять у собі велику силу узагальненого програмування. Абстрактні концепції на кшталт Hashable
характеризують типи з точки зору їх концептуальних характеристик, допомагаючи уникати використання конкретних типів.
Синтаксис обмежень типів
Обмеження типів записуються шляхом вказування назви класу чи протоколу у списку параметрів типів, після назви параметру типу, відділяючись від нього комою. Базовий синтаксис обмеження типів узагальненої функції показано нижче (синтаксис для узагальнених функцій та типів однаковий):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// тут йде тіло функції
}
Гіпотетична функція вище має два параметри типів. Перший параметр типу, T
, має обмеження типу, що вимагає T
бути класом-нащадком класу SomeClass
. Другий параметр, U
, має обмеження типу, що вимагає U
бути підпорядкованим протоколу SomeProtocol
.
Обмеження типів у дії
Ось приклад не узагальненої функції на ім’я findIndex(ofString:in:)
, котра шукає заданий рядок у заданому масиві рядків. Функція findIndex(ofString:in:)
повертає опціональне цілочисельне значення, котре виражає індекс першого рядка в масиві, що співпав із заданим, або nil
, якщо не співпав жоден:
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
Функцію findIndex(ofString:in:)
можна використовувати для знаходження рядку у масиві рядків:
let strings = ["кіт", "пес", "лама", "папуга", "черепаха"]
if let foundIndex = findIndex(ofString: "лама", in: strings) {
print("Рядок 'лама' має індекс \(foundIndex)")
}
// Надрукує "Рядок 'лама' має індекс 2"
Однак, принцип знаходження індексу значення в масиві може стати в пригоді не лише для рядків. Можна написати аналогічну функціональність у вигляді узагальненої функції, замінюючи згадки типу String
на параметр типу T
.
Ось як могла б виглядати узагальнена версія функції findIndex(ofString:in:)
, що носила б назву findIndex(of:in:)
. Слід помітити, що тип, що повертається цією функцією, все ще є Int?
, оскільки функція повертає опціональний індекс у масиві, а не опціональне значення з масиву. Однак, маємо попередити: дана функція не скомпілюється через причини, які ми пояснимо далі після цього прикладу:
func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
Функція вище не скомпілюється. Проблема лежить в перевірці на рівність, “if value == valueToFind
”. Не кожен тип у Swift можна порівнювати за допомогою оператору рівності (==
). Якщо, наприклад, створити власний клас чи структуру, що представляє якусь модель даних, тоді значення операції “дорівнює” для цього класу чи структури не є очевидним для Swift, і компілятор не може його вгадати. Через це, неможливо гарантувати, що даний код буде працювати для будь-якого можливого типу T
, і компілятор вкаже на це у повідомленні про помилку.
Однак, не все втрачено. У стандартній бібліотеці Swift є протокол, що має назву Equatable
, котрий вимагає в підпорядкованого типу реалізовувати оператори рівності (==
) та нерівності (!=
) для порівняння двох значень даного типу. Всі стандартні типи Swift автоматично підпорядковані протоколу Equatable
.
Будь-який тип, підпорядкований протоколу Equatable
, можна безпечно використовувати у функції findIndex(of:in:)
, оскільки він гарантовано підтримує оператор рівності. Для вираження цього факту, вимога підпорядковуватись протоколу Equatable
записується як обмеження типу T
у визначенні функції:
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
Єдиний параметр типу функції findIndex(of:in:)
записується як T: Equatable
, що означає “будь-який T
, що підпорядковується протоколу Equatable
.
Функція findIndex(of:in:)
тепер успішно компілюється та може використовуватись із будь-яким підпорядкованим протоколу Equatable
типом, наприклад, Double
чи String
:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex є опціональним Int без значення, бо значення 9.3 відсутнє в масиві
let stringIndex = findIndex(of: "Антоніна", in: ["Михайло", "Максим", "Антоніна"])
// stringIndex є опціональним Int зі значенням 2
Асоційовані типи
При визначенні протоколу, іноді буває зручно оголошувати один чи декілька асоційованих типів у оголошенні протоколу. Асоційовані типи виступають замісником назви типу, що може використовуватись у протоколі. Фактичний тип заміщує асоційований тип в момент підпорядкування протоколу якимось типом. Асоційовані типи оголошуються за допомогою ключового слова associatedtype
.
Асоційовані типи в дії
Ось приклад протоколу, що має назву Container
та оголошує асоційований тип на ім’я Item
:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Протокол Container
визначає три необхідні можливості, що повинен надавати будь-який контейнер:
- Додавати нові елементи в контейнер за допомогою методу
append(_:)
. - Дізнаватись про кількість елементів у контейнері за допомогою цілочисельної властивості для читання
count
- Отримувати елемент з контейнера за цілочисельним індексом.
Даний протокол не визначає, як повинні зберігатися елементи в контейнері, чи якого типу вони повинні бути. Протокол визначає лише три кавалки функціональності, котрі повинний надати тип для того, щоб вважатись контейнером. Підпорядкований тип може надавати й додаткову функціональність, однак при цьому він повинен задовольняти дані три вимоги.
Будь-який підпорядкований протоколові Container
тип повинен вказати тип значення, що в ньому зберігатимуться. Зокрема, він має гарантувати, що додати до контейнера можна лише елементи правильного типу, і що дістати з контейнера за індексом можна лише елементи цього ж типу.
Щоб визначити ці вимоги, протокол Container
повинен посилатись на тип елементів, що зберігатимуться в контейнері, не знаючи якого конкретно вони будуть типу. Протокол Container
повинен вказати, що будь-яке значення, що передається до методу append(_:)
повинно мати той же тип, що зберігається у контейнері, і що значення, котре повертається індексом контейнера теж має той же тип, що зберігається у контейнері.
Щоб досягти цього, у протоколі Container
оголошується асоційований тип на ім’я Item
, що записується як associatedtype Item
. Протокол не визначає, чим є Item
: право визначати цю інформацію залишається за підпорядкованим типом. Але разом з тим, псевдонім Item
надає спосіб посилатись на тип елементів у протоколі Container
: визначати тип, що передається до методу append(_:)
та повертається індексом, і таким чином змушувати будь-який Container
мати очікувану поведінку.
Ось приклад не узагальненого типу IntStack
з підрозділу Узагальнені типи вище, котрий було підпорядковано протоколу Container
:
struct IntStack: Container {
// початкова реалізація IntStack
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// підпорядкування протоколу Container
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
Тип IntStack
реалізовує всі три вимоги протоколу Container
, і в кожному випадку вже існуюча частина функціональності IntStack
обгортається для задоволення цих вимог.
Навіть більше, структура IntStack
тепер визначає, що для даної реалізації протоколу Container
, тип Int
грає роль типу Item
. Визначення псевдоніму typealias Item = Int
перетворює абстрактний тип Item
на конкретний тип Int
для цієї реалізації протоколу Container
.
Завдяки визначенню типів у Swift, фактично не потрібно визначати конкретний Item
типу Int
в оголошенні IntStack
. Оскільки IntStack
підпорядковується до усіх вимог протоколу Container
, Swift може визначати відповідний тип для використання у ролі Item
, просто поглянувши на тип параметра методу append(_:)
, або тип, що повертається індексом. Справді, якщо з коду видалити рядок із псевдонімом типу typealias Item = Int
, він все одно працюватиме, оскільки зрозуміло, який тип слід використовувати в якості Item
.
Узагальнений тип Stack
можна також підпорядкувати протоколу Container
:
struct Stack<Element>: Container {
// початкова реалізація Stack<Element>
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// підпорядкування протоколу Container
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
Цього разу, як тип параметру методу append(_:)
, та тип, що повертається індексом, використовується тип Element
. В цьому випадку визначення типів у Swift знову допомагає встановити, що в якості типу Item
слід використовувати тип Element
.
Розширення існуючого типу для визначення асоційованого типу
Існуючі типи можна розширювати для підпорядкування їх певному протоколу, як це описано у підрозділі Підпорядкування протоколу за допомогою розширення. Це стосується також і протоколів з асоційованими типами.
Тип Array
у Swift вже має метод append(_:)
, властивість count
та цілочисельний індекс, що повертає його елементи. Ці три можливості співпадають з вимогами протоколу Container
. Це означає, що можна розширити масив, підпорядковуючи його протоколові Container
, просто оголосивши, що Array
є підпорядкований протоколу. Це можна зробити за допомогою порожнього розширення, що описано у підрозділі Оголошення підпорядкування протоколу за допомогою розширення:
extension Array: Container {}
Існуючі метод append(_:)
та цілочисельний індекс дозволяють Swift визначити правильний тип для використання в якості асоційованого типу Item
, так само, як і у випадку з узагальненим типом Stack
вище. Після визначення цього розширення, будь-який масив можна використовувати як Container
.
Додавання обмежень на асоційований тип
Можна додати обмеження типу на асоційований тип у протоколі, щоб вимагати у підпорядкованих типів задоволення цих обмежень.
У протоколах обмеження типу на асоційовані типи додають для того, щоб вимагати у підпорядкованих типів задовольняти умови цих обмежень. Наприклад, у наступному прикладі оголошено версію протоколу Container
, що вимагає, щоб елементи в контейнері підпорядковувались протоколу Equatable
.
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
Щоб підпорядковуватись даній версії протоколу Container
, тип Item
контейнера повинен підпорядковуватись протоколу Equatable
.
Використання протоколів з обмеженнями асоційованих типів
Протокол може з’являтись як частина власних обмежень. Наприклад, ось протокол, що удосконалює протокол Container
, додаючи до вимог метод suffix(_:)
. Метод suffix(_:)
повертає задану кількість елементів з кінця контейнера, зберігаючи їх в екземплярі типу Suffix
.
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
У цьому протоколі, Suffix
є асоційованим типом, як і тип Item
у протоколі Container
у прикладах вище. Асоційований тип Suffix
має два обмеження: він має підпорядковуватись протоколу SuffixableContainer
(протоколу, що в даний момент оголошується), та його тип Item
повинен співпадати із типом Item
даного контейнера. Обмеження на Item
задається за допомогою інструкції узагальнення where, що описана далі у підрозділі Асоційовані типи з інструкцією узагальнення Where.
Ось розширення типу Stack
із підрозділу Узагальнені типи, котре підпорядковує його до протоколу SuffixableContainer
:
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Тип Suffix визначено як Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix містить елементи 20 та 30
У прикладі вище, асоційованим типом Suffix
для структури Stack
є також Stack
, таким чином операція suffix
на типі Stack
повертає інший Stack
. Однак, підпорядкований протоколу SuffixableContainer
тип може мати Suffix
, що не співпадає з цим же типом: іншими словами, операція suffix
може повертати інший тип. Наприклад, ось розширення не узагальненого типу IntStack
, що підпорядковує його протоколові SuffixableContainer
, використовуючи тип Stack<Int>
як тип Suffix
замість типу IntStack
:
extension IntStack: SuffixableContainer {
func suffix(_ size: Int) -> Stack<Int> {
var result = Stack<Int>()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
// Тип Suffix визначено як Stack<Int>.
}
Інструкція узагальнення Where
Обмеження типів, описані у підрозділі Обмеження типів вище, дозволяють визначати обмеження на параметри типу узагальнених функцій, індексів чи типів.
Також буває потрібно визначати вимоги до асоційованих типів. Це можна зробити за допомогою інструкції узагальнення where. Інструкція узагальнення where дозволяє вимагати, щоб асоційований тип підпорядковувався певному протоколу, або щоб певні параметри типів та асоційовані типи були однаковими. Інструкція узагальнення where починається з ключового слова where
, після чого йдуть обмеження на асоційовані типи, чи відношення рівності між типами та асоційованими типами. Інструкції узагальнення where записуються прямо перед фігурною дужкою, з якої починається тіло типу чи функції.
У прикладі нижче визначено узагальнену функцію, що називається allItemsMatch
, котра перевіряє, чи співпадають елементи двох контейнерів, тобто чи містять два контейнери однакові елементи в одному й тому ж порядку. Дана функція повертає булеве значення true
, якщо всі елементи співпадають, та false
, якщо ні.
Два контейнери, що перевіряються, не обов’язково повинні бути одного типу (хоч вони й можуть буту одного типу), але вони повинні містити елементи одного типу. Цю вимогу можна виразити за допомогою комбінації обмеження типів та інструкції узагальнення where:
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// Перевіряємо, що обидва контейнери містять однакову кількість елементів.
if someContainer.count != anotherContainer.count {
return false
}
// Перевіряємо пари елементів, щоб переконатись, що вони еквівалентні.
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// Якщо всі елементи співпадають, повертаємо true.
return true
}
Ця функція приймає два аргументи, що називаються someContainer
та anotherContainer
. Аргумент someContainer
має тип C1
, а аргумент anotherContainer
– C2
. Як C1
, так і C2
є параметрами типу, що є типами-контейнерами, і конкретні їх типи будуть визначені під час виклику функції.
До двох параметрів типу даної функції виставляються наступні вимоги:
C1
має підпорядковуватись протоколуContainer
(записується якC1: Container
).C2
також має підпорядковуватись протоколуContainer
(записується якC2: Container
).- Тип
Item
дляC1
має співпадати із типомItem
для C2 (записується якC1.Item == C2.Item
). - Тип
Item
дляC1
має підпорядковуватись протоколуEquatable
(записується якC1.Item: Equatable
).
Перша та друга вимоги оголошуються прямо у списку параметрів типу функції, а третя та четверта вимоги оголошуються в інструкції узагальнення where даної функції.
Ці вимоги означають, що:
someContainer
є контейнером типуC1
.anotherContainer
є контейнером типуC2
.someContainer
таanotherContainer
містять елементи одного типу.- Елементи у
someContainer
можна перевірити на нерівність за допомогою оператору нерівності!=
, щоб дізнатись, чи відрізняються вони один від одного.
Поєднання третьої та четвертої вимог означає, що елементи у anotherContainer
також можна перевірити на нерівність за допомогою оператору !=
, оскільки вони мають точно такий же тип, що й елементи у someContainer
.
Ці вимоги дозволяють функції allItemsMatch(_:_:)
порівнювати два контейнери, навіть якщо вони мають різний тип.
Функція allItemsMatch(_:_:)
розпочинається із перевірки, що обидва контейнери мають однакову кількість елементів. Якщо контейнери містять різну кількість елементів, вони ні в якому разі не можуть співпадати, і функція повертає false
.
Після цієї перевірки, функція ітерує усі елементи у контейнері someContainer
за допомогою циклу for
-in
та напіввідкритого оператора діапазону (..<
). Для кожного елемента, функція перевіряє, чи дорівнює елемент із someContainer
його відповіднику із anotherContainer
. Якщо два елементи не співпадають, тоді й два контейнери не співпадають, і функція повертає false
.
Якщо цикл закінчується, не знайшовши неспівпадіння, тоді обидва контейнери співпадають, і функція повертає true
.
Ось як функція allItemsMatch(_:_:)
виглядає у дії:
var stackOfStrings = Stack<String>()
stackOfStrings.push("bir")
stackOfStrings.push("eki")
stackOfStrings.push("üç")
var arrayOfStrings = ["bir", "eki", "üç"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("Всі елементи співпадають.")
} else {
print("Не всі елементи співпадають.")
}
// Надрукує "Всі елементи співпадають."
У прикладі вище створено екземпляр Stack
, що зберігає значення типу String
, і в нього заштовхнуто три рядки. Далі у прикладі створено масив, що ініціалізовано літералом масиву, котрий містить ці ж само три рядки, що було заштовхнуто до стека. Хоч Stack
та Array
- різні типи, вони обидва підпорядковуються протоколу Container
, і обидва містять значення одного й того ж типу. Тому можна викликати функцію allItemsMatch(_:_:)
із цими двома контейнерами в якості аргументів. У прикладі вище, функція allItemsMatch(_:_:)
коректно визначає, що всі елементи у цих двох контейнерах співпадають.
Розширення з інструкцією узагальнення Where
Інструкцію узагальнення where
можна також використовувати як частину розширення. У прикладі нижче узагальнену структуру Stack
з попередніх прикладів розширено, додаючи у неї метод isTop(_:)
.
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
Цей новий метод isTop(_:)
спершу перевіряє, чи не є стек порожнім, і потім порівнює передане значення з елементом на горі стеку. Якщо спробувати зробити це без інструкції узагальнення where, буде проблема: реалізація функції isTop(_:)
використовує оператор ==
, але визначення структури Stack
не вимагає, щоб її елементи були порівнюваними, тому використання оператору ==
призводить до помилки часу компіляції. Використання інструкції узагальнення where дозволяє додати нову вимогу до розширення, таким чином дане розширення додає функцію isTop(_:)
лише для тоді, коли елементи у Stack
підпорядковані протоколу Equatable
.
Ось як виглядає у дії метод isTop(_:)
:
if stackOfStrings.isTop("üç") {
print("Елементом на горі є üç.")
} else {
print("Елементом на горі є щось інше.")
}
// Надрукує "Елементом на горі є üç".
Якщо спробувати викликати метод isTop(_:)
на стеку, чиї елементи не є порівнюваними, отримаємо помилку компіляції:
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Помилка
Інструкцію узагальнення where можна також використовувати у розширеннях протоколів. У прикладі нижче протокол Container
з попередніх прикладів розширено методом startsWith(_:)
.
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
Метод startsWith(_:)
спершу упевнюється в тому, що контейнер містить хоча б один елемент, і потім перевіряє, чи співпадає передане значення з першим елементом у контейнері. Цей новий метод startsWith(_:)
можна тепер використовувати із будь-яким підпорядкованим протоколу Container
типом, включно зі стеками та масивами, що використовувались вище. Єдиною вимогою для цього є підпорядкування елементів контейнера протоколу Equatable
.
if [9, 9, 9].startsWith(42) {
print("Починається із 42.")
} else {
print("Починається із чогось іще.")
}
// Надрукує "Починається із чогось іще."
Інструкція узагальнення where у прикладі вище вимагає, щоб тип Item
підпорядковувався протоколу, але можна також написати інструкцію узагальнення where, що вимагатиме, щоб тип Item
був певним типом. Наприклад:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// Надрукує "648.9"
У даному прикладі до усіх контейнерів, чиїм типом Item
є Double
, додається метод average()
, котрий підраховує середнє арифметичне значення усіх елементів контейнера. Для цього цей метод ітерує усі елементи у контейнері, підраховуючи їх суму, і ділить цю суму на кількість елементів у контейнері. У ньому значення count
явно перетворюється із Int
на Double
для уможливлення ділення.
Для розширень також можна вказувати декілька вимог в одній інструкції узагальнення where, як це можна робити у будь-якій інструкції узагальнення where. Можна вимога у списку повинна відділятися комою.
Контекстуальна інструкція узагальнення Where
Інструкцію узагальнення where
можна використовувати як частину оголошення, що не має власних обмежень типів, коли ви уже працюєте у контексті узагальненого типу. Наприклад, інструкцію узагальнення where
можна вказати на індексі узагальненого типу, або на методі у розширенні узагальненого типу. Структура Container
у прикладі нижче є узагальненою, і інструкція узагальнення where
вказує, які обмеження типу мають задовольнятися аби ці нові методи були доступними на її екземплярах.
extension Container {
func average() -> Double where Item == Int {
var sum = 0.0
for index in 0..<count {
sum += Double(self[index])
}
return sum / Double(count)
}
func endsWith(_ item: Item) -> Bool where Item: Equatable {
return count >= 1 && self[count-1] == item
}
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// Надрукує "648.75"
print(numbers.endsWith(37))
// Надрукує "true"
У цьому прикладі до структури Container
додається метод average()
для випадків, коли елементи є цілими числами, та метод endsWith(_:)
, для випадків, коли елементи можна перевіряти на рівність. Обидві функції мають у своєму оголошенні інструкцію узагальнення where
, котра додає обмеження на параметр типу Item
із початкового оголошення структури Container
.
Якщо ви захочете написати цей код без контекстуальної інструкції узагальнення where
, вам слід буде написати два різних розширення, кожне зі своєю інструкцією узагальнення where
. Приклад вище та приклад нижче описують ідентичну поведінку.
extension Container where Item == Int {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += Double(self[index])
}
return sum / Double(count)
}
}
extension Container where Item: Equatable {
func endsWith(_ item: Item) -> Bool {
return count >= 1 && self[count-1] == item
}
}
У версії цього прикладу, що використовує контекстуальну інструкцію узагальнення where
, обидві реалізації методів average()
та endsWith(_:)
містяться в одному розширенні, оскільки інструкції where
кожного методу зазначають вимоги, які мають задовольнятись, щоб відповідний метод був доступним. Перенесення цих вимог до інструкцій where
розширень робить ці методи доступними у цих же само ситуаціях, але змушує створювати по одному розширенню на кожен метод.
Асоційовані типи з інструкцією узагальнення Where
Інструкція узагальнення where може застосовуватись до асоційованого типу. Наприклад, припустимо, що вам потрібно створити версію протоколу Container
, що містить у собі ітератор, аналогічно до протоколу Sequence
зі стандартної бібліотеки. Ось як це можна записати:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
Інструкція узагальнення where на асоційованому типі Iterator
вимагає, щоб ітератор переходив по елементах того ж типу, що містяться в контейнері, незалежно від типу самого контейнера. Функція makeIterator()
надає доступ до ітератора контейнера.
Якщо протокол наслідується від іншого протоколу, до нього можна додавати обмеження на успадкований асоційований тип, включаючи інструкцію узагальнення where до оголошення протоколу. Наприклад, у наступному коді оголошується протокол ComparableContainer
, що вимагає, щоб тип Item
був підпорядкованим протоколу Comparable
:
protocol ComparableContainer: Container where Item: Comparable { }
Узагальнені індекси
Індекси також можуть бути узагальненими, і вони також можуть мати інструкцію узагальнення where. Назву замісника типу записують всередині кутових дужок після ключового слова subscript
, а інструкцію узагальнення where – прямо перед кутовою дужкою, з якої починається тіло індексу. Наприклад:
extension Container {
subscript<Indices: Sequence>(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
Це розширення протоколу Container
додає до нього індекс, що приймає послідовність індексів у контейнері, та повертає масив, що містить елементи за кожним із переданих індексів. Цей узагальнений індекс має наступні обмеження:
- Параметр типу
Indices
у кутових дужках повинен мати тип, що підпорядковується протоколуSequence
зі стандартної бібліотеки. - Індекс приймає єдиний параметр,
indices
, котрий є екземпляром цього типуIndices
. - Інструкція узагальнення where вимагає, щоб ітератор послідовності
Indices
переходив по елементах типуInt
. Це гарантує, що індекси у послідовностіindices
матимуть той же тип, що й індекси у контейнері.
Узяті разом, дані обмеження означають, що передане як параметр indices
значення є послідовністю цілих чисел.