Как стать автором
Обновить
290.95
Рейтинг
OTUS
Цифровые навыки от ведущих экспертов

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

Блог компании OTUSРазработка под iOSSwift
Перевод
Автор оригинала: Aaina Jain
Всем привет! Делимся с вами переводом, подготовленным специально для студентов курса «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-таблицы используются чтобы динамически отправлять вызовы функции в нужные реализации во время выполнения.

Конец первой части. По устоявшейся традиции ждем ваши комментарии, друзья.

Вторая часть
Теги:SwiftGenericsGeneric ProgrammingSwift 4iOS App Development
Хабы: Блог компании OTUS Разработка под iOS Swift
Всего голосов 17: ↑13 и ↓4+9
Просмотры11K

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
otus.ru
Численность
51–100 человек
Дата регистрации
Представитель
OTUS

Блог на Хабре