Универсальные шаблоны (generic) являются одним из мощнейших инструментов Swift. На их основе написано большинство библиотек. Даже если вы никогда специально не использовали универсальные шаблоны, на самом деле вы взаимодействовали с ними практически в каждой написанной программе.
Универсальные шаблоны позволяют создавать гибкие конструкции (функции, объектные типы) без привязки к конкретному типу данных. Вы лишь описываете требования и функциональные возможности, а Swift самостоятельно определяет, каким типам данных доступен разработанный функционал. Примером может служить тип данных Array (массив). Элементами массива могут выступать значения произвольных типов данных, и для этого разработчикам не требуется создавать отдельные типы массивов: Array<Int>, Array<String> и т.д. Для реализации коллекции использован универсальный шаблон, позволяющий при необходимости указать требования к типу данных. Так, например, в реализации типа Dictonary существует требование, чтобы тип данных ключа соответствовал протоколу Hashable (его предназначение мы обсуждали ранее).
Разработаем функцию, с помощью которой можно поменять значения двух целочисленных переменных (листинг 35.1).
Листинг 35.1
func swapTwoInts( a: inout Int, b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var firstInt = 4010
var secondInt = 13
swapTwoInts(a: &firstInt, b: &secondInt)
firstInt // 13
secondInt // 4010
Функция swapTwoInts(a:b:) использует сквозные параметры, чтобы обеспечить доступ непосредственно к параметрам, передаваемым в функцию, а не к их копиям. В результате выполнения значения в переменных firstInt и secondInt меняются местами.
Данная функция является крайне полезной, но очень ограниченной в своих возможностях. Для того чтобы поменять значения двух переменных других типов, придется писать отдельную функцию: swapTwoStrings(), swapTwoDoubles() и т.д. Если обратить внимание на то, что тела всех функций должны быть практически одинаковыми, то мы просто-напросто займемся дублированием кода, хотя ранее в книге неоднократно рекомендовалось всеми способами этого избегать.
Для решения задачи было бы намного удобнее использовать универсальную функцию, позволяющую передать в качестве аргумента значения любого типа с одним лишь требованием: типы данных обоих аргументов должны быть одинаковыми.
Универсальные функции объявляются точно так же, как и стандартные, за одним исключением: после имени функции в угловых скобках указывается заполнитель имени типа, то есть литерал, который далее в функции будет указывать на тип данных переданного аргумента.
Преобразуем функцию swapTwoInts(a:b:) к универсальному виду (листинг 35.2).
Листинг 35.2
func swapTwoValues<T>( a: inout T, b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
var firstString = "one"
var secondString = "two"
swapTwoValues(a: &firstString, b: &secondString)
firstString // "two"
secondString // "one"
В универсальной функции заполнителем типа является литерал T, который и позволяет задать тип данных в списке входных аргументов вместо конкретного типа (Int, String и т.д.). При этом определяется, что a и b должны быть одного и того же типа данных.
Функция swapTwoValues(a:b:) может вызываться точно так же, как и определенная ранее функция swapTwoInts(a:b:).
Используемый заполнитель называется параметром типа. Как только вы его определили, можете применять его для указания типа любого параметра или значения, включая возвращаемое функцией значение. При необходимости можно задать несколько параметров типа, вписав их в угловых скобках через запятую.
В дополнение к универсальным функциям универсальные шаблоны позволяют создать универсальные типы данных. К универсальным типам относятся, например, упомянутые ранее массивы и словари.
Создадим универсальный тип данных Stack (стек) — упорядоченную коллекцию элементов, подобную массиву, но со строгим набором доступных операций:
• метод push(_:) служит для добавления элемента в конец коллекции;
• метод pop() служит для возвращения элемента из конца коллекции с удалением его оттуда.
Никаких иных доступных операций для взаимодействия со своими элементами стек не поддерживает.
В первую очередь создадим неуниверсальную версию данного типа (листинг 35.3).
Листинг 35.3
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
Данный тип обеспечивает работу исключительно со значениями типа Int. В качестве хранилища элементов используется массив [Int]. Сам тип для взаимодействия с элементами коллекции предоставляет нам оба описанных ранее метода.
Недостатком созданного типа является его ограниченность в отношении типа используемого значения. Реализуем универсальную версию типа, позволяющую работать с любыми однотипными элементами (листинг 35.4).
Листинг 35.4
struct Stack<T> {
var items = [T]()
mutating func push(_ item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
Универсальная версия отличается от неуниверсальной только тем, что вместо указания конкретного типа данных задается заполнитель имени типа.
Создавая новую коллекцию типа Stack, в угловых скобках необходимо указать тип данных, после чего можно использовать описанные методы для модификации хранилища (листинг 35.5).
Листинг 35.5
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
let fromTheTop = stackOfStrings.pop() // "dos"
В коллекцию типа Stack<String> были добавлены два элемента и удален один.
Мы можем доработать описанный тип данных таким образом, чтобы при создании хранилища не было необходимости указывать тип элементов стека (листинг 35.6). Для реализации этой задачи опишем инициализатор, принимающий в качестве входного аргумента массив значений.
Листинг 35.6
struct Stack<T> {
var items = [T]()
init(){}
init(_ elements: T...){
self.items = elements
}
mutating func push(_ item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
var stackOfInt = Stack(1, 2, 4, 5)
type(of:stackOfInt) // Stack<Int>.Type
var stackOfStrings = Stack<String>()
type(of:stackOfStrings) // Stack<String>.Type
Так как мы объявили собственный инициализатор, принимающий входной параметр, для сохранения функциональности пришлось описать также пустой инициализатор.
Теперь мы можем не создавать стек без указания типа элементов, а просто передать значения в качестве входного аргумента в инциализатор типа.
Иногда бывает полезно указать определенные ограничения, накладываемые на типы данных универсального шаблона. В качестве примера мы уже рассматривали тип данных Dictionary, где для ключа существует требование: тип данных должен соответствовать протоколу Hashable.
Универсальные шаблоны позволяют накладывать определенные требования и ограничения на тип данных значения. Вы можете указать список типов, которым должен соответствовать тип значения. Если элементом этого списка является протокол (который также является типом данных), то проверяется соответствие типа значения данному протоколу; если типом является класс, структура или перечисления, то проверяется, соответствует ли тип значения данному типу.
Для определения ограничений необходимо передать перечень имен типов через двоеточие после заполнителя имени типа.
Реализуем функцию, производящую поиск элемента в массиве и возвращающую его индекс (листинг 35.7).
ПРИМЕЧАНИЕ Для обеспечения функционала сравнения двух значений в Swift существует специальный протокол Equatable. Он обязывает поддерживающий его тип данных реализовать функционал сравнения двух значений с помощью операторов равенства (==) и неравенства (!=). Другими словами, если тип данных поддерживает этот протокол, то его значения можно сравнивать между собой.
Листинг 35.7
func findIndex<T: Equatable>(array: [T], valueToFind: T) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
var myArray = [3.14159, 0.1, 0.25]
let firstIndex = findIndex(array: myArray, valueToFind: 0.1) // 1
let secondIndex = findIndex(array: myArray, valueToFind: 31) // nil
Параметр типа записывается как <T: Equatable>. Это означает «любой тип, поддерживающий протокол Equatable». В результате поиск в переданном массиве выполняется без ошибок, поскольку тип данных Int поддерживает протокол Equatable, следовательно, значения данного типа могут быть приняты к обработке.
Swift позволяет расширять описанные универсальные типы. При этом имена заполнителей, использованные в описании типа, могут указываться и в расширении.
Расширим описанный ранее универсальный тип Stack, добавив в него вычисляемое свойство, возвращающее верхний элемент стека без его удаления (листинг 35.8).
Листинг 35.8
extension Stack {
var topItem: T? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
stackOfInt.topItem // 5
stackOfInt.push(7)
stackOfInt.topItem // 7
Свойство topItem задействует заполнитель имени типа T для указания типа свойства. Данное свойство является опционалом, так как значение в стеке может отсутствовать. В этом случае возвращается nil.
При определении протокола бывает удобно использовать связанные типы, указывающие на некоторый, пока неизвестный, тип данных. Связанный тип позволяет задать заполнитель типа данных, который будет использоваться при заполнении протокола. Фактически тип данных не указывается до тех пор, пока протокол не будет принят каким-либо объектным типом. Связанные типы указываются с помощью ключевого слова associatedtype, за которым следует имя связанного типа.
Определим протокол Container, использующий связанный тип ItemType (листинг 35.9).
Листинг 35.9
protocol Container {
associatedtype ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Протокол Container (контейнер) может быть задействован в различных коллекциях, например в описанном ранее типе коллекции Stack. В этом случае тип данных, используемый в свойствах и методах протокола, заранее неизвестен.
Для решения проблемы используется связанный тип ItemType, который определяется лишь при принятии протокола типом данных.
Пример принятия протокола к исполнению типом данных Stack представлен в листинге 35.10.
Листинг 35.10
struct Stack<T>: Container {
typealias ItemType = T
var items = [T]()
var count: Int {
return items.count
}
init(){}
init(_ elements: T...){
self.items = elements
}
subscript(i: Int) -> T {
return items[i]
}
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
mutating func append(item: T) {
items.append(item)
}
}
Так как тип Stack теперь поддерживает протокол Container, в нем появилось три новых элемента: свойство, метод и сабскрипт. Ключевое слово typealias указывает на то, какой тип данных является связанным в данном объектном типе.
ПРИМЕЧАНИЕ Обратите внимание на то, что при описании протокола используется ключевое слово associatedtype, а при описании структуры — typealias.
Так как заполнитель имени использован в качестве типа аргумента item свойства append и возвращаемого значения сабскрипта, Swift может самостоятельно определить, что заполнитель T указывает на тип ItemType, соответствующий типу данных в протоколе Container. При этом указывать ключевое слово associatedtype не обязательно: если вы его удалите, то тип продолжит работать без ошибок.