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-таблицы используются чтобы динамически отправлять вызовы функции в нужные реализации во время выполнения.
Конец первой части. По устоявшейся традиции ждем ваши комментарии, друзья.
Комментарии (7)
ivlevAstef
07.08.2019 10:51Все хорошо, вот ток смущает «iOS разработчик. Продвинутый курс».
Не понятно почему простейшая работа с generic-ами стала продвинутым курсов — в swiftbook это есть, и расписано подробней.
При этом в статье нет рассказа о всяких изощрённых использований generic, о том как они интересно с автовывоводом типов сочетаются, ну и о том как внутри работает.flintdemon
07.08.2019 14:59Я не являясь разработчиком iOS и разработчиком вообще, хотя и имею кое какие знания в этом успешно прошел их тестирование на оценку Б, типа ваших знаний достаточно чтобы записаться на курс. Все, что нужно знать о продвинутости.
MaxRokatansky Автор
07.08.2019 15:10Добрый день. Тестирование оценивает лишь базовые знания, для понимания того, потяните ли Вы программу. Для понимания формата обучения можете посмотреть запись открытого урока, а также ознакомиться с программой курса.
MaxRokatansky Автор
07.08.2019 15:11Перевод данной статьи не имеет отношения к программе курса. Это просто интересная информация и не более.
mamkin_developer
А вы не интересовались реальной ситуацией с контейнерами для дженериков? Если я не ошибаюсь, то находя в коде вызов дженерик метода, компилятор просто создает копию метода для конкретного типа. Поэтому количество вызовов дженерик методов может влиять на размер бинарника. Что касается контейнера и выделение памяти, это автор с экземплярами протокольного типа в рантайме путает.
iWheelBuy
Вроде не ошибаетесь. Ellie Shin из Uber неплохо раскрыла эту проблему в своем докладе Putting Your App on a Diet
aknew
Ну вообще если мы возьмем код стека на темплейтах из статьи, сгенерим две переменных с интовым стеком и одну со стринговым, а потом выдернуть у них типы через type(of:), то типы у интовых совпадут, а у стрингового будет другой. Будь там реально контейнер совпали бы все три ИМХО.
К тому же я почти уверен что это все обертка над плюсовыми темплейтами, а в C++ все темплейты разрешаются на этапе компиляции