Универсальные шаблоны

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

17 ноября 2022

 

Универсальные шаблоны одна из самых мощных особенностей Swift, и большая часть всех библиотек Swift построена на основе универсального кода. На самом деле вы используете универсальный код все время, даже если вы этого не осознаете. Например, коллекции Swift Array или Dictionary являются универсальными. Вы можете создать массив, который содержит значения типа Int или массив, который содержит значения String, или на самом деле любой другой массив, который может содержать любой другой тип. Аналогично вы создаете словарь, который может содержать значения разных типов, и нет никакого ограничения по типу хранящихся значений.

Проблема, которую решают универсальные шаблоны

Приведем обычную, стандартную, неуниверсальную функцию swapTwoInts(_:_:), которая меняет два Int местами:


func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

Эта функция использует сквозные параметры для замещения значения a и b, что описано в Сквозные параметры.

Функция swapTwoInts(_:_:) обменивает начальные значения переменных a и b местами. Вы можете использовать эту функцию для замещения двух значений типа Int:


var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Выведет "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(_:_:), и swapTwoDouble(_:_:) идентичны. Единственное отличие в том, что они поддерживают значения различных типов (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>). Угловые скобки говорят Swift, что T является заполнителем имени типа внутри определения функции swapTwoValues(_:_:). Так как T является заполнителем, то Swift не смотрит на текущее значение T.

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

В двух примерах ниже 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(_:_:) в вашем коде, вы можете использовать существующую в Swift функцию swap(_:_:), а не писать свою собственную реализацию.

Параметры типа

В примере выше в функции swapTwoValues(_:_:) заполнитель имени типа T пример параметра типа. Параметры типа определяют и называют тип наполнителя, и пишутся сразу после имени функции, между угловыми скобками (например, <T>).

Как только вы определили параметр типа, то вы можете использовать его в качестве типа параметра функции (как например, параметры a и b в функции swapTwoValues(_:_:)) или как возвращаемый функциональный тип, или как аннотация типа внутри тела функции. В каждом случае заполнитель типа отображается параметром типа, который заменяется на актуальное значение типа при вызове функции. (В swapTwoValues(_:_:) в нашем примере выше произошло замещение Т на Int в момент, когда функция была вызвана в первый раз, и на String при повторном вызове.)

Вы можете использовать несколько параметров типа, просто вписав их в угловых скобках через запятую.

Именование параметров типа

В большинстве случаев параметры типа имеют описательные имена, такие как Key и Value в Dictionary<Key, Value> и Element в Array<Element>, которые помогут читающему код определить взаимосвязь между параметром типа и универсальным типом или функцией, в которых он используется. Тем не менее, когда между ними нет значимых отношений, то по традиции именами становятся отдельные буквы, такие как T, U, V, как например T в функции swapTwoValues(_:_:).

Заметка

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

Универсальные типы

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

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

Заметка

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

Ниже приведена иллюстрация поведения добавления и удаления элемента из стека:

  1. На данный момент у нас три значения в стеке.
  2. Четвертое значение “затолкнули” на самый верх стека.
  3. На этот момент в стеке находится четыре значения, самое свежее значение находится наверху.
  4. Последнее значение удалено или “выстреляно” из стека.
  5. После удаления значения, стек снова имеет три значения.

Вот как написать неуниверсальную версию стека, в этом случае мы используем стек для хранения Int значений:


struct IntStack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

Эта структура использует свойство items типа Array для хранения значений в стеке. 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 похожа на не универсальную, вообще отличаясь только тем, что мы используем заполнитель типа, вместо указания конкретного типа Int. Этот параметр типа написан внутри угловых скобок ( <Element>), сразу после имени структуры.

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")
// stack содержит 4 строки

Теперь stackOfStrings выглядит вот так после добавления последних четырех значений:

Удаляя последнее значение, он возвращает его и удаляет его из стека “cuatro”:


let fromTheTop = stackOfStrings.pop()
// fromTheTop равен "cuatro", а stack содержит 3 строки

После удаления верхней величины, стек выглядит так:

Расширяем универсальный тип

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

Следующий пример расширяет универсальный тип Stack, для добавления вычисляемого свойства только для чтения topItem, которое возвращает верхний элемент стека, без “выстреливания” его из этого стека:


extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}

Свойство topItem возвращает опциональное значение типа Element. Если стек пустой, то topItem возвращает nil. Если стек не пустой, то topItem возвращает последний элемент массива items.

Обратите внимание, что расширение не определяет списка параметров типа. Вместо этого, имя существующего параметра типа Stack - Element, используется внутри расширения для отображения опционального типа вычисляемого свойства topItem.

Вычисляемое свойство topItem теперь может быть использовано внутри экземпляра Stack для доступа к значению и для запроса к последнему элемента стека, без дальнейшего его удаления:


if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Выведет "The top item on the stack is tres."

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

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

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

Для примера возьмем тип Dictionary, который имеет некоторые ограничения типов, которые могут быть использованы в качестве ключей. Как было описано в главе Словари, тип ключа словаря должен быть хешируемым. Таким образом он должен предоставить способ представить себя уникальным. Dictionary нужно, чтобы его ключи были хешируемыми, таким образом он может проверить, содержит ли конкретный ключ какое-либо значение. Без этого требования, 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:), которая получает значение типа 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)")
}
// Выведет "The index of llama is 2"

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


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, например, String, Double:


let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex опциональный Int не имеющий значения, потому что значения 9.3 нет в массиве
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// 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, которое возвращает значение типа Int.
  • Должна быть возможность получить значение через индекс элемента, который принимает значение типа Int.

Этот протокол не указывает количество и способ хранения элементов в контейнере или какого типа они должны быть. Протокол только лишь указывает три “кусочка” функциональности, которые должны быть предоставлены контейнером, чтобы он считался Container. Соответствующий тип может предоставлять дополнительную функциональность, пока он удовлетворяет этим трем требованиям.

Любой тип, который удовлетворяет протоколу Equatable должен иметь возможность указывать на тип хранящихся элементов. Конкретно, он должен гарантировать, что только элементы правильного типа будут добавлены в контейнер, и должно быть ясно какой тип элементов будет возвращаться сабскриптом.

Для определения этих требований, протокол 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 указывает, что для этой реализации контейнера, подходящий тип Item будет Int. Определение typealias Item = Int преобразует абстрактный тип Item в конкретный тип Int для этой реализации протокола Container.

Благодаря выводу типов Swift, вам фактически не нужно указывать конкретный тип Int для Item как часть определения IntStack. Так как IntStack соответствует протоколу Container, Swift может вывести соответствующий тип для Item, просто посмотрев на тип параметра 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]
    }
}

В этот раз тип параметра Element использован в качестве параметра item метода append(_:) и в качестве возвращаемого типа сабскрипта. Таким образом Swift может вывести, что Element подходящий тип для использования его в качестве типа Item для этого конкретного контейнера.

Расширение существующего типа для указания связанного типа

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

Тип Array уже предоставляет нам метод append(_:), свойство count и сабскрипт со значением индекса типа Int для получения его элементов. Эти три возможности удовлетворяют и совпадают с требованиями протокола Container. Это означает, что вы можете расширить тип Array, чтобы он соответствовал протоколу Container, просто указав, что Array принимает протокол Container. Вы можете сделать это при помощи пустого расширения, которое подробнее описано в подразделе главы Принятие протокола через расширение:


extension Array: Container {}

Существующий метод append(_:) типа Array и сабскрипт позволяют Swift выводить соответствующий тип для Item, точно так же как и для универсального типа Stack, который был приведен ранее. После определения расширения вы можете использовать Array как Container.

Добавление ограничений в связанный тип

Вы можете добавить ограничение к связанному типу в протоколе, чтобы требовать, чтобы соответствующие типы удовлетворяли этим ограничениям. Например, следующий код определяет версию Container, который требует, чтобы его элементы реализовывали протокол Equatable.


protocol Container {
    associatedtype Item: Equatable
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Для соответствия данной версии Container, каждому элементу Container нужно соответствовать/реализовывать протокол Equatable.

Использование протокола в ограничениях связанного типа

Протокол может выступать как часть собственных требований. Например, ниже в примере, есть протокол, который уточняет протокол Container, добавляя метод suffix(_:). Метод suffix(_:) возвращает заданное количество элементов из конца контейнера, сохраняя их в экземпляре типа Suffix.


protocol SuffixableContainer: Container {
    associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
    func suffix(_ size: Int) -> Suffix
}

В этом протоколе Suffix является ассоциированным типом, подобным типу Item в приведенном выше примере. Suffix имеет два ограничения: он должен соответствовать протоколу SuffixableContainer (протокол, который в настоящее время определяется), а его тип Item должен быть таким же, как тип Item контейнера. Ограничение на Item является общей оговоркой where, которое обсуждается ниже.

Ниже расширение типа Stack, которое добавляет соответствие протоколу SuffixableContainer:


extension Stack: SuffixableContainer {
    func suffix(_ size: Int) -> Stack {
        var result = Stack()
        for index in (count-size)..()
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 в качестве суффиксного типа, вместо 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 сразу после списка параметров типа, за которым следует одно или более ограничений для связанных типов, и/или один или более отношений равенства между типами и связанными типами.

В примере ниже определяем универсальную функцию allItemsMatch, которая проверяет, чтобы увидеть содержат ли два экземпляра Container одни и те же элементы в одной и той же последовательности. Функция возвращает значение типа Bool, то есть, если у нас все элементы и их последовательность совпадает, то функция возвращает 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, и записаны после ключевого слова 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("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.")
}
// Выведет "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(_:) сначала проверяет, что наш стек не пустой, а затем сравнивает верхний элемент стека с данным нам item. Если вы попытаетесь сделать то же самое без универсальной where, то у вас будут проблемы. Реализация  isTop(_:) использует оператор ==, но определение Stack не требует того, чтобы элементы могли сравниваться, так что написание оператора  == вызовет ошибку компиляции. Использование универсальной  where позволяет вам добавить новое требование к расширению, так что расширение добавляет метод isTop(_:) только в том случае, если элементы в Stack реализуют Equatable.

Вот как будет выглядеть наш метод isTop(_:) в действии:


if stackOfStrings.isTop("tres") {
    print("Top element is tres.")
} else {
    print("Top element is something else.")
}
// Выведет "Top element is tres."

Если вы попытаетесь вызвать метод isTop(_:) в стеке, то те, элементы, которые не реализуют протокол Equatable вызовут ошибку компиляции:


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


if [9, 9, 9].startsWith(42) {
    print("Starts with 42.")
} else {
    print("Starts with something else.")
}
// Выведет "Starts with something else."

Универсальная оговорка where в примере выше требует, чтобы Item соответствовал протоколу Container, но вы так же можете использовать 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"

Этот пример добавляет контейнерам метод average(), у которых Item имеет тип Double. Он итерирует по элементам контейнера, суммируя их, и делит сумму на их общее количество. Он явно преобразует count из Int в Double что позволяет нам проводить деление чисел с плавающей точкой.

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

В этом примере мы добавляем методы average() в структуру Container, когда элементы имеют целочисленный тип, а так же мы добавляем метод endsWith(_:), если элементы соответствуют протоколу Equatable. Оба метода включают в себя оговорку where, которая добавляет ограничения по типу для Item из оригинального объявления структуры Container.

Если вы хотите написать этот код без контекстуальной оговорки where, вам нужно написать два расширения, по одному для каждой оговорки where. Пример выше и пример ниже имеют полностью идентичное поведение.


extension Container where Item == Int {
    func average() -> Double {
        var sum = 0.0
        for index in 0.. Bool {
        return count >= 1 && self[count-1] == item
    }
}

В ранее приведенном примере, который использует контекстуальную оговорку where, реализация методов average() и endsWith(_:)  размещена в одном расширении структуры, потому что в каждом случае универсальная оговорка 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 требует, чтобы итератор последовательности мог перемещаться по элементам типа Int. Это гарантирует, что индексы последовательности того же самого типа, что и индексы, которые использовались в контейнере.

Беря все это во внимание, ограничения, которые описаны выше означают, что значения, передаваемые для параметра indices, должны быть последовательностью элементов типа Int.

Содержание