Добрый день, друзья. Специально для студентов курса «iOS Разработчик. Продвинутый курс» мы подготовили перевод второй части статьи «Сила дженериков в Swift».
Связанные типы, условия where, сабскрипты и прочее…
В статье «Сила дженериков в Swift. Часть 1» описывались generic-функции, generic-типы и ограничения типа. Если вы новичок, я бы рекомендовала вам для лучшего понимания сначала прочитать первую часть.
При определении протокола иногда полезно объявить один или несколько связанных типов как часть определения. Связанный тип задает имя-заглушку для типа, который используется в качестве части протокола. Фактический тип, используемый для этого связанного типа не будет указан, пока протокол не будет использован. Связанные типы объявляются с помощью ключевого слова
Мы можем определить протокол для стека, который мы создали в первой части.
Протокол
Любой тип, соответствующий протоколу
Давайте изменим наш стек в соответствии с протоколом:
Вы можете расширить существующий тип, чтобы обеспечить соответствие протоколу.
Вы можете добавить ограничения к связанному типу в протоколе для гарантии соответствия связанных типов этим ограничениям.
Давайте изменим протокол
Теперь тип элемента стека должен соответствовать
Протокол может являться частью собственных требований.
В стандартной библиотеке Swift в
Предложение об ограничениях рекурсивного протокола: https://github.com/apple/swift-evolution/blob/master/proposals/0157-recursive-protocol-constraints.md
Для связанных типов бывает полезно определить требования. Требование описывается generic-условием where. Generic-условие
Вы можете использовать generic-условие
Расширение добавляет метод
Вы можете включить generic-условие
Для протокола, который наследуется от другого протокола, вы добавляете ограничение к унаследованному связанному типу, включая generic-условие
Алиас типов может иметь общие параметры. Он все еще будет оставаться псевдонимом (то есть он не будет вводить новый тип).
В данном механизме нельзя использовать дополнительные ограничения к параметрам типа.
Такой код не заработает:
Сабскрипты могут использовать механизм дженериков и включать generic-условие
Специализация дженериков означает, что компилятор клонирует универсальный тип или функцию, такую как Stack
Специализируя generic-функцию для этих типов, мы можем исключить затраты на виртуальную диспетчеризацию, инлайн вызовы, когда это необходимо, и устранить накладные расходы дженерик системы.
Generic-типы по умолчанию не работают с операторами, для этого вам нужен протокол.
Почему вы не можете определить статическое хранимое свойство для универсального типа?
Это потребует отдельного хранения свойств для каждой отдельной специализации дженерика (T).
https://github.com/apple/swift/blob/master/docs/Generics.rst
https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md
https://developer.apple.com/videos/play/wwdc2018/406/
https://www.youtube.com/watch?v=ctS8FzqcRug
https://medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1
На этом все. До встречи на курсе.
Связанные типы, условия where, сабскрипты и прочее…
В статье «Сила дженериков в Swift. Часть 1» описывались generic-функции, generic-типы и ограничения типа. Если вы новичок, я бы рекомендовала вам для лучшего понимания сначала прочитать первую часть.
При определении протокола иногда полезно объявить один или несколько связанных типов как часть определения. Связанный тип задает имя-заглушку для типа, который используется в качестве части протокола. Фактический тип, используемый для этого связанного типа не будет указан, пока протокол не будет использован. Связанные типы объявляются с помощью ключевого слова
associatedtype
.Мы можем определить протокол для стека, который мы создали в первой части.
protocol Stackable {
associatedtype Element
mutating func push(element: Element)
mutating func pop() -> Element?
func peek() throws -> Element
func isEmpty() -> Bool
func count() -> Int
subscript(i: Int) -> Element { get }
}
Протокол
Stackable
определяет необходимый функционал, который должен обеспечивать любой стек.Любой тип, соответствующий протоколу
Stackable
, должен иметь возможность указывать тип значений, которые он хранит. Он должен гарантировать, что в стек добавляются только элементы правильного типа, и должно быть ясно, элементы какого типа возвращаются его сабскриптом.Давайте изменим наш стек в соответствии с протоколом:
enum StackError: Error {
case Empty(message: String)
}
protocol Stackable {
associatedtype Element
mutating func push(element: Element)
mutating func pop() -> Element?
func peek() throws -> Element
func isEmpty() -> Bool
func count() -> Int
subscript(i: Int) -> Element { get }
}
public struct Stack<T>: Stackable {
public typealias Element = T
var array: [T] = []
init(capacity: Int) {
array.reserveCapacity(capacity)
}
public mutating func push(element: T) {
array.append(element)
}
public mutating func pop() -> T? {
return array.popLast()
}
public func peek() throws -> T {
guard !isEmpty(), let lastElement = array.last else {
throw StackError.Empty(message: "Array is empty")
}
return lastElement
}
func isEmpty() -> Bool {
return array.isEmpty
}
func count() -> Int {
return array.count
}
}
extension Stack: Collection {
public func makeIterator() -> AnyIterator<T> {
var curr = self
return AnyIterator { curr.pop() }
}
public subscript(i: Int) -> T {
return array[i]
}
public var startIndex: Int {
return array.startIndex
}
public var endIndex: Int {
return array.endIndex
}
public func index(after i: Int) -> Int {
return array.index(after: i)
}
}
extension Stack: CustomStringConvertible {
public var description: String {
let header = "***Stack Operations start*** "
let footer = " ***Stack Operation end***"
let elements = array.map{ "\($0)" }.joined(separator: "\n")
return header + elements + footer
}
}
var stack = Stack<Int>(capacity: 10)
stack.push(element: 1)
stack.pop()
stack.push(element: 3)
stack.push(element: 4)
print(stack)
Расширение существующего типа для указания связанного типа
Вы можете расширить существующий тип, чтобы обеспечить соответствие протоколу.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
extension Array: Container {}
Добавление ограничений в связанный тип:
Вы можете добавить ограничения к связанному типу в протоколе для гарантии соответствия связанных типов этим ограничениям.
Давайте изменим протокол
Stackable
.protocol Stackable {
associatedtype Element: Equatable
mutating func push(element: Element)
mutating func pop() -> Element?
func peek() throws -> Element
func isEmpty() -> Bool
func count() -> Int
subscript(i: Int) -> Element { get }
}
Теперь тип элемента стека должен соответствовать
Equatable
, иначе произойдет ошибка времени компиляции.Рекурсивные ограничения протокола:
Протокол может являться частью собственных требований.
protocol SuffixableContainer: Container {
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
Suffix
имеет два ограничения: он должен соответствовать протоколу SuffixableContainer
(здесь определяется протокол), а его тип Item
должен совпадать с типом Item
контейнера.В стандартной библиотеке Swift в
Protocol Sequence
есть хороший пример, иллюстрирующий эту тему.Предложение об ограничениях рекурсивного протокола: https://github.com/apple/swift-evolution/blob/master/proposals/0157-recursive-protocol-constraints.md
Расширения Generic-типа:
Когда вы расширяете generic-тип, вы не описываете список параметров типа при определении расширения. Вместо этого список параметров типа из исходного определения доступен в теле расширения, а имена параметров исходного типа используются для ссылки на параметры типа из исходного определения.
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
Generic-условие where
Для связанных типов бывает полезно определить требования. Требование описывается generic-условием where. Generic-условие
where
позволяет вам требовать, чтобы связанный тип соответствовал определенному протоколу или чтобы определенные параметры типа и связанные типы были одинаковыми. Generic-условие where
начинается с ключевого слова where
, за которым следуют ограничения для связанных типов или отношения равенства между типами и связанными типами. Generic-условие where
пишется прямо перед открывающей фигурной скобкой тела типа или функции.func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
}
Расширения с Generic-условиями where
Вы можете использовать generic-условие
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 (_ :)
только тогда, когда элементы в стеке поддерживают Equatable. Также вы можете использовать generic-условие where
с расширениями протокола. К условию where
можно добавить несколько требований, разделив их запятой.Связанные типы с Generic-условием where:
Вы можете включить generic-условие
where
в связанный тип.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
}
Для протокола, который наследуется от другого протокола, вы добавляете ограничение к унаследованному связанному типу, включая generic-условие
where
в объявление протокола. Например, следующий код объявляет протокол ComparableContainer
, который требует, чтобы Item
поддерживал Comparable
:protocol ComparableContainer: Container where Item: Comparable { }
Дженерик алиасы типов:
Алиас типов может иметь общие параметры. Он все еще будет оставаться псевдонимом (то есть он не будет вводить новый тип).
typealias StringDictionary<Value> = Dictionary<String, Value>
var d1 = StringDictionary<Int>()
var d2: Dictionary<String, Int> = d1 // okay: d1 and d2 have the same type, Dictionary<String, Int>
typealias DictionaryOfStrings<T : Hashable> = Dictionary<T, String>
typealias IntFunction<T> = (T) -> Int
typealias Vec3<T> = (T, T, T)
typealias BackwardTriple<T1,T2,T3> = (T3, T2, T1)
В данном механизме нельзя использовать дополнительные ограничения к параметрам типа.
Такой код не заработает:
typealias ComparableArray<T where T : Comparable> = Array<T>
Generic-сабскрипты
Сабскрипты могут использовать механизм дженериков и включать generic-условие
where
. Вы пишете имя типа в угловых скобках после subscript
, и пишете generic-условие 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
}
}
Специализация дженериков
Специализация дженериков означает, что компилятор клонирует универсальный тип или функцию, такую как Stack
<
T>
, для конкретного типа параметра, такого как Int. Эта специализированная функция может быть затем оптимизирована специально для Int, при этом все лишнее будет удалено. Процесс замены параметров типа аргументами типа во время компиляции называется специализацией.Специализируя generic-функцию для этих типов, мы можем исключить затраты на виртуальную диспетчеризацию, инлайн вызовы, когда это необходимо, и устранить накладные расходы дженерик системы.
Перегрузка оператора
Generic-типы по умолчанию не работают с операторами, для этого вам нужен протокол.
func ==<T: Equatable>(lhs: Matrix<T>, rhs: Matrix<T>) -> Bool {
return lhs.array == rhs.array
}
Интересная вещь о дженериках
Почему вы не можете определить статическое хранимое свойство для универсального типа?
Это потребует отдельного хранения свойств для каждой отдельной специализации дженерика (T).
Ресурсы для углубленного изучения:
https://github.com/apple/swift/blob/master/docs/Generics.rst
https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md
https://developer.apple.com/videos/play/wwdc2018/406/
https://www.youtube.com/watch?v=ctS8FzqcRug
https://medium.com/@vhart/protocols-generics-and-existential-containers-wait-what-e2e698262ab1
На этом все. До встречи на курсе.