Сила дженериков в Swift. Часть 1

от автора

Всем привет! Делимся с вами переводом, подготовленным специально для студентов курса «iOS Разработчик. Продвинутый курс». Приятного прочтения.

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *