Spec-Zone.ru › Swift Language

Обобщения

Напишите код, который работает с несколькими типами и укажите требования к этим типам.

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

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

Проблема, которую решают обобщения

Вот стандартная, необобщённая функция под названием swapTwoInts(_:_:), которая меняет местами два Int значения:

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

Эта функция использует параметры ввода-вывода для изменения мест значений a и b, как описано в <doc:Functions#In-Out-Parameters>.

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

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

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

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

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

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

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

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

Обобщённые типы

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

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

Примечание: Концепция стека используется классом UINavigationController для моделирования контроллеров представлений в его иерархии навигации. Вы вызываете метод UINavigationController класса pushViewController(_:animated:), чтобы добавить (или поместить) контроллер представления в стек навигации, и метод 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()
    }
}

Эта структура использует свойство 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

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

Извлечение значения из стека удаляет и возвращает верхнее значение, "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, 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."

Расширения обобщённого типа также могут включать требования, которые должны удовлетворять экземпляры расширяемого типа, чтобы получить новые возможности, как обсуждается в <doc:Generics#Extensions-with-a-Generic-Where-Clause> ниже.

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

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

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

Это требование обеспечивается ограничением типа ключа для Dictionary, которое указывает, что тип ключа должен соответствовать протоколу Hashable, специальному протоколу, определённому в стандартной библиотеке Swift. Все базовые типы Swift (такие как String, Int, Double и Bool) по умолчанию хешируемы. Сведения о том, как сделать ваши собственные пользовательские типы совместимыми с протоколом Hashable, см. на соответствии протоколу 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
}

Эта функция не компилируется в написанном виде. Проблема заключается в проверке на равенство «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, например, 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.

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

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

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

Для достижения этой цели протокол Container объявляет связанный тип, называемый Item, записываемый как associatedtype Item. Протокол не определяет, что такое Item — эта информация предоставляется любым соответствующим типом. Тем не менее, псевдоним Item предоставляет способ ссылки на тип элементов в Container и определения типа для использования с методом append(_:) и индексацией, чтобы гарантировать соблюдение ожидаемого поведения любого Container.

Вот версия необобщенного типа IntStack из <doc:Generics#Generic-Types> выше, адаптированная для соответствия протоколу 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.

Благодаря выводу типов Swift вам на самом деле не нужно объявлять конкретный тип Item типа Int в рамках определения IntStack. Поскольку IntStack соответствует всем требованиям протокола Container, Swift может вывести соответствующий тип Item, просто взглянув на тип параметра item метода append(_:) и тип возвращаемого значения подстроки. Действительно, если вы удалите строку 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 используется как тип параметра item метода append(_:) и тип возвращаемого значения подстроки. Следовательно, Swift может вывести, что Element является подходящим типом для использования в качестве Item для этого конкретного контейнера.

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

Вы можете расширить существующий тип, добавив соответствие протоколу, как описано в <doc:Protocols#Adding-Protocol-Conformance-with-an-Extension>. Это включает протокол со связанным типом.

Тип Swift Array уже предоставляет метод append(_:), свойство count и подстроку с индексом Int для извлечения элементов. Эти три возможности соответствуют требованиям протокола Container. Это означает, что вы можете расширить Array для соответствия протоколу Container, просто объявив, что Array принимает протокол. Это делается с помощью пустого расширения, как описано в <doc:Protocols#Declaring-Protocol-Adoption-with-an-Extension>:

extension Array: Container {}

Существующий метод append(_:) и подстрока массива позволяют 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, тип 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, который обсуждается в <doc:Generics#Associated-Types-with-a-Generic-Where-Clause> ниже.

Вот расширение типа Stack из <doc:Generics#Generic-Types> выше, которое добавляет соответствие протоколу 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 для 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

Ограничения типа, как описано в <doc:Generics#Type-Constraints>, позволяют определить требования к параметрам типа, связанным с обобщенной функцией, индексацией или типом.

Также может быть полезно определить требования к связанным типам. Это делается путём определения обобщенного пункта 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 для C1 должен быть таким же, как связанный тип Item для C2 (записанному как C1.Item == C2.Item).
  • Связанный тип Item для C1 должен соответствовать протоколу Equatable (записанному как C1.Item: Equatable).

Первые два требования определены в списке параметров типа функции, а третьи и четвертые — в обобщенном пункте where функции.

Эти требования означают:

  • someContainer — это контейнер типа C1.
  • anotherContainer — это контейнер типа C2.
  • someContainer и anotherContainer содержат элементы одного типа.
  • Элементы в someContainer можно проверять с помощью оператора `!=` (!=), чтобы определить, отличаются ли они друг от друга.

Третье и четвертое требования объединяются, чтобы означать, что элементы в anotherContainer также можно проверять с помощью оператора !=, потому что они имеют ровно тот же тип, что и элементы в someContainer.

Эти требования позволяют функции allItemsMatch(_:_:) сравнивать два контейнера, даже если они являются контейнерами разных типов.

Функция allItemsMatch(_:_:) начинает с проверки того, что оба контейнера содержат одинаковое количество элементов. Если они содержат разное количество элементов, они не могут совпадать, и функция возвращает false.

После этой проверки функция перебирает все элементы в someContainer с помощью цикла `for-in` и полуоткрытого оператора диапазона (..<). Для каждого элемента функция проверяет, не равен ли элемент из someContainer соответствующему элементу в anotherContainer. Если эти два элемента не равны, контейнеры не совпадают, и функция возвращает false.

Если цикл завершается без обнаружения несоответствия, два контейнера совпадают, и функция возвращает true.

Вот как функция allItemsMatch(_:_:) работает на практике:

var stackOfStrings = Stack<String>()
stackOfStrings.push("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 в качестве части объявления, не имеющего собственных ограничений по типам, когда вы уже работаете в контексте общих типов. Например, вы можете написать общий клауз 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())
// Prints "648.75"
print(numbers.endsWith(37))
// Prints "true"

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

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

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

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

Связанные типы с общим клаузом where

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

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 из стандартной библиотеки Swift.
  • Подскрипт принимает один параметр, indices, который является экземпляром этого типа Indices.
  • Общий клауз where требует, чтобы итератор последовательности проходил по элементам типа Int. Это гарантирует, что индексы в последовательности имеют тот же тип, что и индексы, используемые для контейнера.

Вместе эти ограничения означают, что значение, переданное для параметра indices, является последовательностью целых чисел.

This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API