
Generic-функция, generic-тип и ограничения типа
Что такое дженерики?
Когда они работают – вы их любите, а когда нет – ненавидите!
В реальной жизни все знают силу дженериков: просыпаясь утром, решая, что пить, наполняя чашку.
️
Swift – это типобезопасный язык. Всякий раз, когда мы работаем с типами, нам нужно явно их указывать. Например, нам нужна функция, которая будет работать более чем с одним типом. Swift имеет типы Any и AnyObject, но их стоит использовать осторожно и далеко не всегда. Использование Any и AnyObject сделает ваш код ненадежным, поскольку будет невозможно отследить несоответствие типов при компиляции. Именно тут на помощь приходят дженерики.
Generic код позволяет создавать многократно используемые функции и типы данных, которые могут работать с любым типом, отвечающем определенным ограничениям, обеспечивая при этом типобезопасность во время компиляции. Этот подход позволяет писать код, который помогает избежать дублирования и выражает свой функционал в понятной абстрактной манере. Например, такие типы как Array, Set и Dictionary используют дженерики для хранения элементов.
Скажем, нам нужно создать массив, состоящий из значений целого типа и строк. Чтобы решить эту задачу, я создам две функции.
let intArray = [1, 2, 3, 4] let stringArray = [a, b, c, d] func printInts(array: [Int]) { print(intArray.map { $0 }) } func printStrings(array: [String]) { print(stringArray.map { $0 }) }
Теперь мне нужно вывести массив элементов типа float или массив пользовательских объектов. Если мы посмотрим на функции выше, то увидим, что используется только разница в типе. Поэтому вместо того, чтобы дублировать код, мы можем написать generic-функцию для повторного использования.
История дженериков в Swift

Generic-функции
Generic-функция может работать с любым универсальным параметром типа T. Имя типа ничего не говорит о том, каким должно быть Т, но оно говорит, что оба массива должны быть типа Т, независимо от того, что Т из себя представляет. Сам тип для использования вместо Т определяется каждый раз при вызове функции .print(_:)
func print<T>(array: [T]) { print(array.map { $0 }) }
Универсальные типы или параметрический полиморфизм
Универсальный тип Т из примера выше – это параметр типа. Можно указать несколько параметров типа, записав несколько имен параметров типа в угловые скобки, разделив их запятыми.
Если посмотреть на Array и Dictionary<Key, Element>, то можно заметить, что у них есть именованные параметры типа, то есть Element и Key, Element, которые говорит о связи между параметром типа и generic-типом или функцией, в которой он используется.
Примечание: Всегда давайте имена параметрам типа в нотации СamelCase (например, T и TypeParameter), чтобы показать, что они являются названием для типа, а не значением.
Generic-типы
Это пользовательские классы, структуры и перечисления, которые могут работать с любым типом, аналогично массивам и словарям.
Давайте создадим стек
import Foundation enum StackError: Error { case Empty(message: String) } public struct Stack { var array: [Int] = [] init(capacity: Int) { array.reserveCapacity(capacity) } public mutating func push(element: Int) { array.append(element) } public mutating func pop() -> Int? { return array.popLast() } public func peek() throws -> Int { guard !isEmpty(), let lastElement = array.last else { throw StackError.Empty(message: "Array is empty") } return lastElement } func isEmpty() -> Bool { return array.isEmpty } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) stack.pop() stack.pop() stack.push(element: 5) stack.push(element: 3) stack.push(element: 4) print(stack)
Сейчас этот стек способен принимать только целочисленные элементы, и если мне понадобится хранить элементы другого типа, то нужно будет либо создавать другой стек, либо преобразовывать этот к generic виду.
enum StackError: Error { case Empty(message: String) } public struct Stack<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 } } extension Stack: CustomStringConvertible { public var description: String { let elements = array.map{ "\($0)" }.joined(separator: "\n") return elements } } var stack = Stack<Int>(capacity: 10) stack.push(element: 1) stack.push(element: 2) print(stack) var strigStack = Stack<String>(capacity: 10) strigStack.push(element: "aaina") print(strigStack)
Ограничения Generic-типов
Поскольку дженерик может быть любого типа, многого с ним сделать не получится. Иногда полезно применять ограничения к типам, которые могут использоваться с generic-функциями или generic-типами. Ограничения типа указывают на то, что параметр типа должен соответствовать определенному протоколу или составу протокола.
Например, тип Dictionary в Swift накладывает ограничения на типы, которые могут использоваться в качестве ключей для словаря. Словарь требует, чтобы ключи были хэшируемыми для того, чтобы иметь возможность проверить, содержит ли он уже значения для определенного ключа.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { // function body goes here }
По сути, мы создали стек типа Т, но мы не можем сравнивать два стека, поскольку здесь типы не соответствуют Equatable. Нам нужно изменить это, чтобы использовать .Stack<T: Equatable>
Как работают дженерики? Посмотрим на пример.
func min<T: Comparable>(_ x: T, _ y: T) -> T { return y < x ? y : x }
Компилятору не хватает двух вещей, необходимых для создания кода функции:
- Размеров переменных типа Т;
- Адреса конкретной перегрузки функции <, которая должна вызываться во время выполнения.
Всякий раз, когда компилятор встречает значение, которое имеет тип generic, он помещает значение в контейнер. Этот контейнер имеет фиксированный размер для хранения значения. В случае, если значение слишком велико, Swift аллоцирует его в куче и сохраняет ссылку на него в контейнере.
Компилятор также поддерживает список из одной или нескольких witness-таблиц для каждого generic-параметра: одна witness-таблица для значений, плюс по одной witness-таблице для каждого протокола-ограничения на тип. Witness-таблицы используются чтобы динамически отправлять вызовы функции в нужные реализации во время выполнения.
Конец первой части. По устоявшейся традиции ждем ваши комментарии, друзья.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/462753/
Добавить комментарий