Общий код позволяет вам писать гибкие, многократно используемые функции и типы, которые могут работать с любым типом, в соответствии с определенными вами требованиями. Вы можете написать код, который избегает дублирования и выражает свое намерение в ясной, абстрактной манере.
Обобщения являются одной из самых мощных функций Swift, и большая часть стандартной библиотеки Swift построена с использованием общего кода. На самом деле, вы использовали дженерики в руководстве по языку , даже если не понимали этого. Например, Swift's Array
и Dictionary
типы являются общими коллекциями. Вы можете создать массив, который содержит Int
значения, или массив, который содержит String
значения, или действительно массив для любого другого типа, который может быть создан в Swift. Точно так же вы можете создать словарь для хранения значений любого указанного типа, и нет никаких ограничений в отношении того, каким может быть этот тип.
Проблема, которую решают дженерики
Вот стандартная неуниверсальная функция swapTwoInts(_:_:)
, которая меняет два Int
значения:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
Эта функция использует входные и выходные параметры для обмена значениями a
и b
.
swapTwoInts(_:_:)
Функция обменивает исходное значение b
INTO a
, и исходное значение a
INTO b
. Вы можете вызвать эту функцию, чтобы поменять значения в двух Int
переменных:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 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
представляет. Фактический тип, который следует использовать вместо, T
определяется каждый раз, когда swapTwoValues(_:_:)
вызывается функция.
Другое отличие между универсальной функцией и неуниверсальной функцией заключается в том, что за именем универсальной функции ( swapTwoValues(_:_:)
) следует имя-заполнитель типа ( T
) внутри угловых скобок ( <T>
). Скобки сообщают Swift, что T
это имя типа заполнителя в swapTwoValues(_:_:)
определении функции. Поскольку T
Swift является заполнителем, Swift не ищет фактический вызываемый тип T
.
Теперь swapTwoValues(_:_:)
функцию можно вызывать так же, как swapTwoInts
, за исключением того, что ей могут быть переданы два значения любого типа, если оба эти значения имеют одинаковый тип друг с другом. Каждый раз, когда swapTwoValues(_:_:)
вызывается, тип для использования T
выводится из типов значений, передаваемых в функцию.
В двух приведенных ниже примерах T
подразумевается Int
и String
соответственно:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
ЗАМЕТКА
swapTwoValues(_:_:)
Функция , определенная выше, вдохновленный родовая функция под названиемswap
, которая является частью стандартной библиотеки Swift, и автоматически становится доступным для использования в приложениях. Если вам нужно поведениеswapTwoValues(_:_:)
функции в вашем собственном коде, вы можете использовать существующуюswap(_:_:)
функцию Swift вместо предоставления собственной реализации.
Параметры типа
В swapTwoValues(_:_:)
приведенном выше примере тип заполнителя T
является примером параметра типа . Параметры типа указывают и присваивают имя типу заполнителя и записываются сразу после имени функции между парой совпадающих угловых скобок (например, <T>
).
После того, как вы указали параметр типа, вы можете использовать его для определения типа параметров функции (таких как a
и b
параметры swapTwoValues(_:_:)
функции), или как тип возвращаемого значения функции, или как аннотация типа в теле функции. В каждом случае параметр типа заменяется фактическим типом при каждом вызове функции. (В swapTwoValues(_:_:)
приведенном выше примере T
был заменен при Int
первом вызове функции, и был заменен при String
втором вызове.)
Можно указать несколько параметров типа, написав несколько имен параметров типа в угловых скобках, разделенных запятыми.
Параметры типа именования
В большинстве случаев параметры типа имеют описательные имена, такие как Key
и Value
in и in , что говорит читателю о связи между параметром типа и универсальным типом или функцией, в которой он используется. Однако, когда между ними нет значимых отношений , это традиционное назвать их , используя отдельные буквы , такие как , и , например, в приведенной выше функции.Dictionary<Key, Value>
Element
Array<Element>
T
U
V
T
swapTwoValues(_:_:)
ЗАМЕТКА
Всегда задавайте параметры типа в верхнем регистре имен верблюдов (например,
T
иMyTypeParameter
), чтобы указать, что они являются заполнителями для типа , а не значения.
Общие типы
В дополнение к универсальным функциям Swift позволяет вам определять свои собственные универсальные типы . Это пользовательские классы, структуры и перечисления, которые могут работать с любым типом, аналогично Array
и Dictionary
.
В этом разделе показано, как написать общий тип коллекции с именем Stack
. Стек - это упорядоченный набор значений, аналогичный массиву, но с более ограниченным набором операций, чем Array
тип Swift . Массив позволяет вставлять и удалять новые элементы в любом месте массива. Стек, однако, позволяет новые элементы должны быть приложены только к концу коллекции (известный как толкая новое значение в стек). Аналогично, стек позволяет удалять элементы только из конца коллекции (это называется переносом значения из стека).
ЗАМЕТКА
Концепция стека используется
UINavigationController
классом для моделирования контроллеров представления в его иерархии навигации. Вы вызываете методUINavigationController
классаpushViewController(_:animated:)
для добавления (или добавления) контроллера представления в стек навигации и егоpopViewControllerAnimated(_:)
метод для удаления (или извлечения) контроллера представления из стека навигации. Стек - это полезная модель коллекции, когда вам нужен строгий подход «первым пришел - первым вышел» для управления коллекцией.
В настоящее время в стеке есть три значения.
- Четвертое значение помещается на вершину стека.
- В стеке теперь хранятся четыре значения, самое последнее из которых находится вверху.
- Верхний элемент в стеке выталкивается.
- После получения значения в стеке снова сохраняются три значения.
Вот как написать неуниверсальную версию стека, в данном случае для стека Int
значений:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
Эта структура использует Array
свойство, вызываемое items
для хранения значений в стеке. Stack
предоставляет два метода, push
и pop
, чтобы толкать и извлекать значения из стека и обратно. Эти методы помечены как mutating
, потому что они должны изменить (или мутируют ) структуры в items
массив.
Однако 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")
// the stack now contains 4 strings
Извлечение значения из стека удаляет и возвращает верхнее значение "cuatro"
:
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
Расширение универсального типа
Когда вы расширяете универсальный тип, вы не предоставляете список параметров типа как часть определения расширения. Вместо этого список параметров типа из исходного определения типа доступен в теле расширения, а имена параметров исходного типа используются для ссылки на параметры типа из исходного определения.
В следующем примере расширяется универсальный Stack
тип для добавления вызываемого вычисляемого свойства только для чтения topItem
, которое возвращает верхний элемент в стеке, не извлекая его из стека:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
topItem
Свойство возвращает значение дополнительного типа Element
. Если стек пуст, topItem
возвращает nil
; если стек не пустой, topItem
возвращает последний элемент в items
массиве.
Обратите внимание, что это расширение не определяет список параметров типа. Вместо этого Stack
существующее имя параметра типа type Element
используется в расширении для указания необязательного типа topItem
вычисляемого свойства.
topItem
Вычисленное свойство теперь может быть использован с любым , Stack
например , чтобы получить доступ и запросить его верхний элемент , не удаляя его.
- if let topItem = stackOfStrings.topItem {
- print("The top item on the stack is \(topItem).")
- }
- // Prints "The top item on the stack is tres."
Расширения универсального типа могут также включать требования, которым должны удовлетворять экземпляры расширенного типа, чтобы получить новые функциональные возможности.
Тип Ограничения
swapTwoValues(_:_:)
Функции и Stack
тип может работать с любым типом. Однако иногда полезно применять определенные ограничения типов для типов, которые можно использовать с универсальными функциями и универсальными типами. Ограничения типа указывают, что параметр типа должен наследоваться от определенного класса или соответствовать определенному протоколу или составу протокола.
Например, Dictionary
тип Swift накладывает ограничение на типы, которые можно использовать в качестве ключей для словаря. Как описано в словарях , тип ключей словаря должен быть hashable . То есть он должен обеспечивать способ сделать себя уникально представимым. Dictionary
его ключи должны быть хешируемыми, чтобы он мог проверить, содержит ли оно значение для определенного ключа. Без этого требования Dictionary
он не мог бы сказать, следует ли ему вставлять или заменять значение для определенного ключа, а также не мог бы найти значение для данного ключа, который уже находится в словаре.
Это требование обеспечивается ограничением типа для типа ключа для Dictionary
, которое указывает, что тип ключа должен соответствовать Hashable
протоколу, специальному протоколу, определенному в стандартной библиотеке Swift. Все основные типы Свифта (например String
, Int
, Double
, и Bool
) являются hashable по умолчанию.
Вы можете определить свои собственные ограничения типов при создании пользовательских универсальных типов, и эти ограничения предоставляют большую часть возможностей универсального программирования. Такие абстрактные понятия, как Hashable
характеристики, характеризуют типы с точки зрения их концептуальных характеристик, а не их конкретного типа.
Тип Синтаксис ограничения
Вы пишете ограничения типа, помещая один класс или ограничение протокола после имени параметра типа, разделенного двоеточием, как часть списка параметров типа. Основной синтаксис для ограничений типов для универсальной функции показан ниже (хотя синтаксис для универсальных типов одинаков):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
Вышеуказанная гипотетическая функция имеет два типа параметров. Первый параметр типа T
, имеет ограничение типа, которое должно T
быть подклассом SomeClass
. Второй параметр типа U
, имеет ограничение типа, которое требует U
соответствия протоколу SomeProtocol
.
Введите ограничения в действии
Вот названная неуниверсальная функция findIndex(ofString:in:)
, которой присваивается String
значение для поиска и массив String
значений, в котором ее можно найти. findIndex(ofString:in:)
Функция возвращает необязательное Int
значение, которое будет индексом первой строки соответствия в массиве , если он найден, или 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 = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// Prints "The index of llama is 2"
Однако принцип нахождения индекса значения в массиве полезен не только для строк. Вы можете написать ту же функциональность, что и универсальная функция, заменив любое упоминание строк на значения некоторого типа 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
}
Эта функция не компилируется как написано выше. Проблема заключается в проверке равенства « ». Не каждый тип в Swift можно сравнить с оператором равенства ( ). Если вы создаете свой собственный класс или структуру для представления, например, сложной модели данных, то значение «равно» для этого класса или структуры не является тем, что Свифт может угадать для вас. Из-за этого невозможно гарантировать, что этот код будет работать для каждого возможного типа , и при попытке компилировать код выдается соответствующая ошибка.if value == valueToFind
==
T
Однако не все потеряно. Стандартная библиотека Swift определяет вызываемый протокол Equatable
, для которого требуется, чтобы любой соответствующий тип реализовывал операторы равенства «оператор ( ==
)» и «не равно оператору ( !=
)» для сравнения любых двух значений этого типа. Все стандартные типы Swift автоматически поддерживают Equatable
протокол.
Любой тип, который Equatable
можно безопасно использовать с findIndex(of:in:)
функцией, потому что она гарантированно поддерживает оператор равенства. Чтобы выразить этот факт, вы пишете ограничение типа Equatable
как часть определения параметра типа при определении функции:
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 is an optional Int with no value, because 9.3 isn't in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 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
свойство, которое возвращаетInt
значение. - Должна быть предусмотрена возможность извлечения каждого элемента в контейнере с помощью индекса, который принимает
Int
значение индекса.
Этот протокол не определяет, как элементы в контейнере должны храниться или какого типа они могут быть. Протокол определяет только три бита функциональности, которые должен обеспечивать любой тип для того, чтобы считаться a Container
. Соответствующий тип может обеспечивать дополнительную функциональность, если он удовлетворяет этим трем требованиям.
Любой тип, который соответствует Container
протоколу, должен иметь возможность указывать тип значений, которые он хранит. В частности, он должен гарантировать, что в контейнер добавляются только элементы правильного типа, и должен иметь четкое представление о типе элементов, возвращаемых его индексом.
Чтобы определить эти требования, Container
протоколу нужен способ ссылки на тип элементов, которые будет содержать контейнер, не зная, что это за тип для конкретного контейнера. Container
Протокол должен указать , что любое значение , переданное в append(_:)
метод должен иметь тот же тип, тип элемента контейнера, и что значение , возвращаемое индексом контейнера будет иметь тот же тип, тип элемента контейнера.
Для достижения этого Container
протокол объявляет связанный тип с именем Item
, записанным как . Протокол не определяет, что есть - эта информация оставлена для любого соответствующего типа для предоставления. Тем не менее, псевдоним обеспечивает способ ссылаться на тип элементов в a и определять тип для использования с методом и нижним индексом, чтобы обеспечить принудительное выполнение любого ожидаемого поведения .associatedtype Item
Item
Item
Container
append(_:)
Container
Вот версия неуниверсального IntStack
типа из вышеприведенных универсальных типов , адаптированная для соответствия Container
протоколу:
struct IntStack: Container {
// original IntStack implementation
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
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
, соответствующий Item
для использования является типом Int
. Определение превращает абстрактный тип в конкретный тип для этой реализации протокола.typealias Item = Int
Item
Int
Container
Благодаря умозаключения типа Свифта, вы на самом деле не нужно объявлять конкретный Item
в Int
качестве части определения IntStack
. Поскольку Swift IntStack
соответствует всем требованиям Container
протокола, Swift может определить подходящее Item
использование, просто взглянув на тип параметра append(_:)
метода item
и тип возвращаемого индекса. Действительно, если вы удалите строку из приведенного выше кода, все по-прежнему работает, потому что ясно, для какого типа следует использовать .typealias Item = Int
Item
Вы также можете сделать универсальный Stack
тип соответствующим Container
протоколу:
struct Stack<Element>: Container {
// original Stack<Element> implementation
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
На этот раз параметр типа Element
используется в качестве типа параметра append(_:)
метода item
и возвращаемого типа нижнего индекса. Поэтому Swift может сделать вывод, что Element
это подходящий тип для использования в качестве Item
этого конкретного контейнера.
Расширение существующего типа для указания связанного типа
Вы можете расширить существующий тип, чтобы добавить соответствие протоколу. Это включает в себя протокол со связанным типом.
Array
Тип Swift уже предоставляет append(_:)
метод, count
свойство и индекс с Int
индексом для извлечения его элементов. Эти три возможности соответствуют требованиям Container
протокола. Это означает, что вы можете расширить, Array
чтобы соответствовать Container
протоколу, просто заявив, что Array
принимает протокол.
extension Array: Container {}
Существующий append(_:)
метод и нижний индекс массива позволяют Swift выводить соответствующий тип для использования Item
, так же как и для общего Stack
типа выше. Определив это расширение, вы можете использовать любое Array
как Container
.
Добавление ограничений в связанный тип
Вы можете добавить ограничения типа к связанному типу в протоколе, чтобы требовать, чтобы соответствующие типы удовлетворяли этим ограничениям. Например, следующий код определяет версию, Container
которая требует, чтобы элементы в контейнере были уравниваемыми.
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
предложение.
Вот расширение 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
}
// Inferred that Suffix is Stack.
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix contains 20 and 30
В приведенном выше примере Suffix
ассоциированный тип for Stack
также имеет значение Stack
, поэтому операция суффикса Stack
возвращает другой Stack
. В качестве альтернативы, тип, который соответствует, SuffixableContainer
может иметь Suffix
тип, отличный от самого себя, то есть операция с суффиксами может возвращать другой тип. Например, вот расширение к неуниверсальному IntStack
типу, который добавляет SuffixableContainer
соответствие, используя Stack<Int>
вместо суффикса тип 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
}
// Inferred that Suffix is Stack<Int>.
}
Общие Где Пункты
Ограничения типов позволяют определять требования к параметрам типа, связанным с универсальной функцией, индексом или типом.
Также может быть полезно определить требования для связанных типов. Вы делаете это, определяя универсальное предложение where . Общее where
предложение позволяет вам требовать, чтобы связанный тип соответствовал определенному протоколу или чтобы определенные параметры типа и связанные типы были одинаковыми. Общее where
предложение начинается с where
ключевого слова, за которым следуют ограничения для связанных типов или отношения равенства между типами и связанными типами. Вы пишете общее where
предложение прямо перед открывающей фигурной скобкой тела типа или функции.
В приведенном ниже примере определяется общая вызываемая функция allItemsMatch
, которая проверяет, Container
содержат ли два экземпляра одинаковые элементы в одном и том же порядке. Функция возвращает логическое значение, true
если все элементы совпадают, и значение, false
если они не совпадают .
Два проверяемых контейнера не обязательно должны относиться к одному и тому же типу контейнера (хотя они могут быть), но они должны содержать элементы одного типа. Это требование выражается в комбинации ограничений типа и общего where
предложения:
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
// Check that both containers contain the same number of items.
if someContainer.count != anotherContainer.count {
return false
}
// Check each pair of items to see if they're equivalent.
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// All items match, so return true.
return true
}
Эта функция принимает два аргумента someContainer
и anotherContainer
. someContainer
Аргумент типа C1
, а anotherContainer
аргумент типа C2
. Оба C1
и C2
являются параметрами типа для двух типов контейнеров, которые определяются при вызове функции.
Следующие требования предъявляются к двум типам параметров функции:
C1
должен соответствоватьContainer
протоколу (записывается как ).C1: Container
C2
также должен соответствоватьContainer
протоколу (записывается как ).C2: Container
- Значение
Item
forC1
должно совпадать сItem
forC2
(записывается как ).C1.Item == C2.Item
Item
ДляC1
должно соответствоватьEquatable
протоколу (записывается в виде ).C1.Item: Equatable
Первое и второе требования определены в списке параметров типа функции, а третье и четвертое требования определены в общем where
предложении функции.
Эти требования означают:
someContainer
это контейнер типаC1
.anotherContainer
это контейнер типаC2
.someContainer
иanotherContainer
содержат элементы одного типа.- Элементы в
someContainer
могут быть проверены с помощью оператора equal (!=
), чтобы увидеть, отличаются ли они друг от друга.
Третье и четвертое требования означают, что элементы в такжеanotherContainer
могут быть проверены оператором, потому что они точно такого же типа, как и элементы в .!=
someContainer
Эти требования позволяют allItemsMatch(_:_:)
функции сравнивать два контейнера, даже если они относятся к другому типу контейнера.
allItemsMatch(_:_:)
Функция начинается с проверки того, что оба контейнера содержат одинаковое количество элементов. Если они содержат различное количество элементов, они не могут совпадать, и функция возвращает результат false
.
После выполнения этой проверки функция выполняет итерации по всем элементам someContainer
с помощью цикла for
- in
и оператора полуоткрытого диапазона ( ..<
). Для каждого элемента функция проверяет, someContainer
не равен ли элемент из соответствующего элемента в anotherContainer
. Если два элемента не равны, то два контейнера не совпадают, и функция возвращает false
.
Если цикл завершается без обнаружения несоответствия, два контейнера совпадают, и функция возвращается true
.
Вот как allItemsMatch(_:_:)
выглядит функция в действии:
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// Prints "All items match."
Приведенный выше пример создает Stack
экземпляр для хранения String
значений и помещает три строки в стек. В этом примере также создается 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(_:)
метод только в том случае, если элементы в стеке сопоставимы.
Вот как isTop(_:)
метод выглядит в действии:
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Prints "Top element is tres."
Если вы попытаетесь вызвать isTop(_:)
метод в стеке, элементы которого не уравниваемы, вы получите ошибку во время компиляции.
struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Error
Вы можете использовать общее where
предложение с расширениями протокола. Приведенный ниже пример расширяет Container
протокол из предыдущих примеров, добавляя startsWith(_:)
метод.
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
startsWith(_:)
Метод первого убеждается , что контейнер имеет по меньшей мере один элемент, а затем он проверяет , соответствует ли первый элемент в контейнере данный пункт. Этот новый startsWith(_:)
метод может использоваться с любым типом, который соответствует Container
протоколу, включая стеки и массивы, использованные выше, до тех пор, пока элементы контейнера являются уравновешенными.
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// Prints "Starts with something else."
Общее 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())
// Prints "648.9"
В этом примере average()
метод добавляется в контейнеры с Item
типом Double
. Он перебирает элементы в контейнере, чтобы сложить их, и делит на количество контейнеров, чтобы вычислить среднее. Он явно преобразует число из Int
в, Double
чтобы иметь возможность деления с плавающей запятой.
Вы можете включить несколько требований в универсальное 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
предложение on 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
предложение требует, чтобы итератор для последовательности проходил по элементам типаInt
. Это гарантирует, что индексы в последовательности того же типа, что и индексы, используемые для контейнера.
Взятые вместе, эти ограничения означают, что значение, переданное для indices
параметра, является последовательностью целых чисел.
0 комментариев