swapTwoValues(_:_:)
и тип Stack
могут работать с любым типом. Однако, иногда бывает полезно принудительно задать ограничения на типы, которые могут использоваться с шаблонными функциями и типами. Ограничения типа определяют, что параметр типа должен наследоваться от заданного класса или адаптировать определённый протокол или композицию протоколов.
Dictionary
требует от своих ключей быть хэшируемыми, чтобы проверить, содержится ли в нём или нет значение по заданному ключу. Без этого требования Dictionary
не мог быть сказать, следует ли ему вставить или заменить значения для конкретного ключа, а также не способен найти значение для заданного ключая, который уже находится в словаре.
Dictionary
, который определяет, что этот тип ключа должен соответствовать протоколу Hashable
, специальному протоколу, заданному в стандартной библиотеке Swift. Все базовые типы Swift (такие как String
, Int
, Double
и Bool
) хэшируемы по-умолчанию.
Hashable
характеризуют типы в терминах их концептуальных характеристик, а не их конкретный тип.
Hashable
стандартной библиотеки Swift. Типы, удовлетворяющие протоколу Hashable
должны предоставить получаемое свойство Int
с именем hashValue
. Значение, возвращаемое свойством hashValue
не всегда обязано быть одинаковым между разными запусками одной программы или в разных программах.
Hashable
удовлетворяет протоколу Equatable
, удовлетворяющие типы обязаны также предоставить реализацию оператора равенства (==). Протокол Equatable
требует от любой удовлетворяющей реализации оператора эквивалентности == быть отношением эквивалентности. Это значит, что реализация == должна удовлетворить следующим трём условиям для всех a, b и c:
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
Гипотетическая функция выше имеет два параметра типа. Первый параметр типа, T, имеет ограничение типа, которое требует от T быть подклассом класса SomeClass
. Второй параметр типа, U, имеет ограничение типа, которое требует от U адаптировать протокол SomeProtocol
.
findIndex(ofString:in:)
, которая получает строковые значение для поиска и массива значений 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"
Однако, принцип поиска индекса значения в массиве полезен не только для строк. Вы можете написать ту же функциональность для шаблонной функции, заменив использование строк значениями какого-то типа 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, так что подходящая ошибка будет выведена, когда Вы попытаетесь собрать этот код.
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
определяет три требуемых возможности, которые должен представить любой контейнер:
Container
. Подходящий тип может предоставить дополнительную функциональность, пока он удовлетворяет этим трём требованиям.
Container
, должен иметь возможность определить хранимые в нём типы. Конкретно он должен быть уверен, что только элементы верного типа добавляются в контейнер, также он должен точно знать тип элементов, возвращаемых его сабскриптом.
Container
должен иметь способ для ссылки на тип элементов, которые будет хранить контейнер без знаний о том, каков этот тип для отдельного контейнера. Протокол Container
должен определить, что любой тип, передаваемый в метод append(_:)
, должен иметь тот же тип, что и тип элемента контейнера, а также что значение, возвращаемое санскритом контейнера будет того же типа, что и тип элемента контейнера.
Container
определяет ассоциированный тип с названием Item
, что записывается как associatedtype Item
, Протокол не определяет, чем на самом деле является Item
: эта информация остаётся на любой адаптирующий тип. Однако ярлык Item
позволяет ссылаться на тип объектов в Container
и определить тип для использования с методом append(_:)
и сабскриптом, чтобы быть уверенными, что ожидаемое поведение Container
гарантируется.
IntStack
, данного ранее, адаптированного для соответствия протоколу Container
:
struct IntStack: Container {
// original IntStack implementation
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
typealias Item = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
Тип IntStack
реализует все три требования протокола Container
, и в каждом случае заворачивает часть имеющейся функциональности типа IntStack
для удовлетворения этим требованиям.
IntStack
определяет для этой реализации Container
, что подходящая Item
для использования имеет тип Int
. Определение typealias Item = Int
превращает абстрактный тип Item
в конкретный тип Int
для этой реализации протокола Container
.
Item
в качестве Int
как часть определения IntStack
. Так как 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
в конкретном контейнере.
Array
уже имеет метод append(_:)
, свойство сount
и сабскрипт с индексом Int
для получения его элементов. Эти три возможности совпадают с требованиями протокола Container
. Это значит, что Вы можете расширить Array
для соответствия протоколу Container
, просо объявив, что Array
адаптирует этот протокол. Вы можете сделать это пустым расширением.
extension Array: Container {}
Существующий метод append(_:)
у массива и сабскрипт позволяют Swift вывести подходящий тип для использования в качестве Item
, как и в случае с шаблонным типом Stack
выше. После определения этого расширения, Вы можете использовать любой Array
как Container
.
Container
, который требует от элементов в контейнере быть сравниваемыми на равенство:
Container
, тип Item
должен удовлетворять протоколу Equatable
.
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
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 {
// 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 are 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 являются параметрами типа для определения при вызове функции.
@bt
Следующие требования применяются к двум параметрам типа функции:
where
.
anotherContainer
так же может быть проверено оператором !=, так как они имеют тот же тип, что и элементы в someContainer
.
allItemsMatch(_:_:)
сравнить эти два контейнера, даже если это два разных типа контейнера.
allItemsMatch(_:_:)
начинается с проверки того, что оба контейнера имеют одинаковое количество элементов. Если они содержат разное количество элементов, то нельзя сказать, что они совпадают и функция возвращает false
.
someContainer
c помощью цикла 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
в качестве части расширения. Пример ниже расширяет шаблонную структуру 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.")
}
// Выведет "Top element is tres."
isTop(_:)
на стэке, чьи элементы не сравнимы на равенство, то Вы получите ошибку времени компиляции.
struct NotEquatable { }
var notEquatableStack = Stack()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Ошибка
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.")
}
// Выведет "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())
// Выведет "648.9"
Этот пример добавляет метод average()
контейнерам, чей тип Item
равен Double
. Он проходит через элементы в контейнере для сложения их, а затем делит сумму контейнера для вычисления среднего значения. Он явно приводит сумму из Int
к Double
, чтобы иметь возможность выполнить деление с плавающей точкой.
where
/ в расширении, как вы делали для блока where
, который Вы писали ранее. Разделите каждое требование в списке с помощью запятых.
where
с ассоциированным типом. Например, предположим, что Вы хотите сделать версию Container
, которая включает итератор как протокол Sequence
, используемый в стандартной библиотеке. Вот, как Вы могли бы его записать:
where
на Iterator
требует, чтобы итератор мог проходить через элементы одного типа, что и элементы контейнера, вне зависимости от типа итератора. Функция makeIterator()
предоставляет доступ к итератору контейнера.
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
в объявление протокола. Например, следующий код объявляет протокол ComparableContainer
, который требует от Item
соответствовать протоколу Comprarable
:
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
добавляет сабскрипт, который принимает последовательность индексов и возвращает массив, содержащий элементы на этих позициях. Шаблонный сабскрипт ограничен следующим образом:
typealias
.
typealias AudioSample = UInt16
var maxAmplitudeFound = AudioSample.max
Здесь, AudioSample
определён как псевдоним для UInt16
. Потому что это псевдоним, то вызов AudioSample.min
на самом деле вызывает UInt16.min
, который предоставляет изначальное значение 0 для maxAmplitudeFound
.