Всем привет! Делимся с вами переводом, подготовленным специально для студентов курса «iOS Разработчик. Продвинутый курс». Приятного прочтения.
Generic-функция, generic-тип и ограничения типа
Когда они работают – вы их любите, а когда нет – ненавидите!
В реальной жизни все знают силу дженериков: просыпаясь утром, решая, что пить, наполняя чашку.
️
Swift – это типобезопасный язык. Всякий раз, когда мы работаем с типами, нам нужно явно их указывать. Например, нам нужна функция, которая будет работать более чем с одним типом. Swift имеет типы
Generic код позволяет создавать многократно используемые функции и типы данных, которые могут работать с любым типом, отвечающем определенным ограничениям, обеспечивая при этом типобезопасность во время компиляции. Этот подход позволяет писать код, который помогает избежать дублирования и выражает свой функционал в понятной абстрактной манере. Например, такие типы как
Скажем, нам нужно создать массив, состоящий из значений целого типа и строк. Чтобы решить эту задачу, я создам две функции.
Теперь мне нужно вывести массив элементов типа float или массив пользовательских объектов. Если мы посмотрим на функции выше, то увидим, что используется только разница в типе. Поэтому вместо того, чтобы дублировать код, мы можем написать generic-функцию для повторного использования.
Generic-функция может работать с любым универсальным параметром типа
Универсальный тип Т из примера выше – это параметр типа. Можно указать несколько параметров типа, записав несколько имен параметров типа в угловые скобки, разделив их запятыми.
Если посмотреть на Array и Dictionary<Key, Element>, то можно заметить, что у них есть именованные параметры типа, то есть Element и Key, Element, которые говорит о связи между параметром типа и generic-типом или функцией, в которой он используется.
Примечание: Всегда давайте имена параметрам типа в нотации СamelCase (например,
Это пользовательские классы, структуры и перечисления, которые могут работать с любым типом, аналогично массивам и словарям.
Давайте создадим стек
Сейчас этот стек способен принимать только целочисленные элементы, и если мне понадобится хранить элементы другого типа, то нужно будет либо создавать другой стек, либо преобразовывать этот к generic виду.
Поскольку дженерик может быть любого типа, многого с ним сделать не получится. Иногда полезно применять ограничения к типам, которые могут использоваться с generic-функциями или generic-типами. Ограничения типа указывают на то, что параметр типа должен соответствовать определенному протоколу или составу протокола.
Например, тип
По сути, мы создали стек типа Т, но мы не можем сравнивать два стека, поскольку здесь типы не соответствуют
Как работают дженерики? Посмотрим на пример.
Компилятору не хватает двух вещей, необходимых для создания кода функции:
Всякий раз, когда компилятор встречает значение, которое имеет тип generic, он помещает значение в контейнер. Этот контейнер имеет фиксированный размер для хранения значения. В случае, если значение слишком велико, Swift аллоцирует его в куче и сохраняет ссылку на него в контейнере.
Компилятор также поддерживает список из одной или нескольких witness-таблиц для каждого generic-параметра: одна witness-таблица для значений, плюс по одной witness-таблице для каждого протокола-ограничения на тип. Witness-таблицы используются чтобы динамически отправлять вызовы функции в нужные реализации во время выполнения.
Конец первой части. По устоявшейся традиции ждем ваши комментарии, друзья.
Вторая часть
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-таблицы используются чтобы динамически отправлять вызовы функции в нужные реализации во время выполнения.
Конец первой части. По устоявшейся традиции ждем ваши комментарии, друзья.
Вторая часть