Общий код позволяет вам писать гибкие, многократно используемые функции и типы, которые могут работать с любым типом, в соответствии с определенными вами требованиями. Вы можете написать код, который избегает дублирования и выражает свое намерение в ясной, абстрактной манере.

Обобщения являются одной из самых мощных функций 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(_:_:)Функция обменивает исходное значение bINTO a, и исходное значение aINTO 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(_:_:)функции идентичны. Единственным отличием является тип значений , которые они принимают ( IntStringи 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, в данном случае) вместо фактического имя типа (например IntStringили Double). Имя типа заполнитель ничего о том, что не говорят , Tдолжно быть, но это действительносказать , что как aи bдолжно быть того же типа T, независимо Tпредставляет. Фактический тип, который следует использовать вместо, Tопределяется каждый раз, когда swapTwoValues(_:_:)вызывается функция.

Другое отличие между универсальной функцией и неуниверсальной функцией заключается в том, что за именем универсальной функции ( swapTwoValues(_:_:)) следует имя-заполнитель типа ( T) внутри угловых скобок ( <T>). Скобки сообщают Swift, что Tэто имя типа заполнителя в swapTwoValues(_:_:)определении функции. Поскольку TSwift является заполнителем, 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и Valuein и in , что говорит читателю о связи между параметром типа и универсальным типом или функцией, в которой он используется. Однако, когда между ними нет значимых отношений , это традиционное назвать их , используя отдельные буквы , такие как , и , например, в приведенной выше функции.Dictionary<Key, Value>ElementArray<Element>TUVTswapTwoValues(_:_:)

ЗАМЕТКА

Всегда задавайте параметры типа в верхнем регистре имен верблюдов (например, Tи MyTypeParameter), чтобы указать, что они являются заполнителями для типа , а не значения.

Общие типы

В дополнение к универсальным функциям Swift позволяет вам определять свои собственные универсальные типы . Это пользовательские классы, структуры и перечисления, которые могут работать с любым типом, аналогично Arrayи Dictionary.

В этом разделе показано, как написать общий тип коллекции с именем Stack. Стек - это упорядоченный набор значений, аналогичный массиву, но с более ограниченным набором операций, чем Arrayтип Swift . Массив позволяет вставлять и удалять новые элементы в любом месте массива. Стек, однако, позволяет новые элементы должны быть приложены только к концу коллекции (известный как толкая новое значение в стек). Аналогично, стек позволяет удалять элементы только из конца коллекции (это называется переносом значения из стека).

ЗАМЕТКА

Концепция стека используется UINavigationControllerклассом для моделирования контроллеров представления в его иерархии навигации. Вы вызываете метод UINavigationControllerкласса pushViewController(_:animated:)для добавления (или добавления) контроллера представления в стек навигации и его popViewControllerAnimated(_:)метод для удаления (или извлечения) контроллера представления из стека навигации. Стек - это полезная модель коллекции, когда вам нужен строгий подход «первым пришел - первым вышел» для управления коллекцией.

В настоящее время в стеке есть три значения.

 

  1. Четвертое значение помещается на вершину стека.
  2. В стеке теперь хранятся четыре значения, самое последнее из которых находится вверху.
  3. Верхний элемент в стеке выталкивается.
  4. После получения значения в стеке снова сохраняются три значения.

Вот как написать неуниверсальную версию стека, в данном случае для стека 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например , чтобы получить доступ и запросить его верхний элемент , не удаляя его.

  1. if let topItem = stackOfStrings.topItem {
  2. print("The top item on the stack is \(topItem).")
  3. }
  4. // Prints "The top item on the stack is tres."

Расширения универсального типа могут также включать требования, которым должны удовлетворять экземпляры расширенного типа, чтобы получить новые функциональные возможности.

Тип Ограничения

swapTwoValues(_:_:)Функции и Stackтип может работать с любым типом. Однако иногда полезно применять определенные ограничения типов для типов, которые можно использовать с универсальными функциями и универсальными типами. Ограничения типа указывают, что параметр типа должен наследоваться от определенного класса или соответствовать определенному протоколу или составу протокола.

Например, Dictionaryтип Swift накладывает ограничение на типы, которые можно использовать в качестве ключей для словаря. Как описано в словарях , тип ключей словаря должен быть hashable . То есть он должен обеспечивать способ сделать себя уникально представимым. Dictionaryего ключи должны быть хешируемыми, чтобы он мог проверить, содержит ли оно значение для определенного ключа. Без этого требования Dictionaryон не мог бы сказать, следует ли ему вставлять или заменять значение для определенного ключа, а также не мог бы найти значение для данного ключа, который уже находится в словаре.

Это требование обеспечивается ограничением типа для типа ключа для Dictionary, которое указывает, что тип ключа должен соответствовать Hashableпротоколу, специальному протоколу, определенному в стандартной библиотеке Swift. Все основные типы Свифта (например StringIntDouble, и 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: EquatableTEquatable

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 ItemItemItemContainerappend(_:)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 = IntItemIntContainer

Благодаря умозаключения типа Свифта, вы на самом деле не нужно объявлять конкретный Itemв Intкачестве части определения IntStack. Поскольку Swift IntStackсоответствует всем требованиям Containerпротокола, Swift может определить подходящее Itemиспользование, просто взглянув на тип параметра append(_:)метода itemи тип возвращаемого индекса. Действительно, если вы удалите строку из приведенного выше кода, все по-прежнему работает, потому что ясно, для какого типа следует использовать .typealias Item = IntItem

Вы также можете сделать универсальный 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 }

}

Чтобы соответствовать этой версии ContainerItemтип контейнера должен соответствовать 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и anotherContainersomeContainerАргумент типа C1, а anotherContainerаргумент типа C2. Оба C1и C2являются параметрами типа для двух типов контейнеров, которые определяются при вызове функции.

Следующие требования предъявляются к двум типам параметров функции:

  • C1должен соответствовать Containerпротоколу (записывается как ).C1: Container
  • C2также должен соответствовать Containerпротоколу (записывается как ).C2: Container
  • Значение Itemfor C1должно совпадать с Itemfor C2(записывается как ).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с помощью цикла forinи оператора полуоткрытого диапазона ( ..<). Для каждого элемента функция проверяет, 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параметра, является последовательностью целых чисел.