Основы Swift / 18.1. Шаблоны


Видео


Шаблоны
Шаблонный код позволяет Вам писать гибкие, повторно используемые функции и типы, которые могут работать с любыми типом, удовлетворяющим требованиям, определённым Вами. Вы можете написать код, который избегает дублирования и выражает свой смысл в понятной, абстрактной манере.

Шаблоны - это одна из наиболее сильных возможностей в Swift и большая часть стандартной библиотеки Swift построена в виде дженерик-кода. Фактически, Вы использовали шаблоны на протяжении всего данного руководства, даже если Вы не замечали этого. Например, типы Swift Array и Dictionary оба являются шаблонными коллекциями. Вы можете создать массив, который содержит значения Int или массив значений String, или массив для любого другого типа, который может быть создан в Swift. Аналогично, Вы можете создать словарь для хранения любого конкретного типа, и нет никаких ограничений на то, каким должен быть этот тип.
Проблема, решаемая шаблонами
Здесь представлена стандартная, нешаблонная функция с названием swapTwoInts(_:_:), которая меняет два значения Int:
@{18.1\1\1}
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
Эта функция использует in-out параметры для взаимного обмена значений a и b.
@{18.1\1\2}
Функция swapTwoInts(_:_:) заменяет изначальное значение b на a, а значение a на b. Вы можете вызвать эту функцию для замены двух значений типа Int:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Выведет "someInt is now 107, and anotherInt is now 3"
@{18.1\1\3}
Функция swapTwoInts(_:_:) полезна, но она может быть использована только со значениями Int. Если Вы хотите обменять два значения типа String или Double, Вам нужно будет написать большей функцией, как например swapTwoStrings(_:_:) и swapTwoDoubles(_:_:), как показано ниже:
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}
Вы могли заметить, что тела функций swapTwoInts(_:_:), swapTwoStrings(_:_:) и swapTwoDoubles(_:_:) идентичны. Единственная разница между ними состоит в типе значений, ими принимаемых (Int, String и Double).

Это будет более полезным и более гибким написать единственную версию функции, которая обменивает значения любых типов. Шаблонный код позволяет Вам написать такую функцию. (Обобщённая версия этих функций определена ниже.)

Во всех этих функциях тип a и b должен быть одинаков. Если a и b имеют разные типы, то невозможно обменять их значения. Swift - это типобезопасный язые и он не позволяет (например) переменным типа String и Double обменяться значениями. Попытка сделать это приведёт к ошибке времени компиляции.
Обобщённые функции
Шаблонные функции могут работать с любым типом. Здесь представлена шаблонная версия функции swapTwoInts(_:_:) из примера выше с названием swapTwoValues(_:_:):
@{18.1\1\4}
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}
@{18.1\1\5}
func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)
Тело функции swapTwoValues(_:_:) идентично телу функции swapTwoInts(_:_:). Однако, первая строчка swapTwoValues(_:_:) существенно отличается от swapTwoInts(_:_:). Здесь представлен пример сравнения первых строчек:

Шаблонная версия функции использует замещаемое имя типа (в данном случае с названием T) вместо конкретного имени типа (как например Int, String или Double). Замещаемое имя типа ничего не говорит о том, каким должен быть этот тип T, но оно говорит о том, что оба объекта a и b должны быть одного типа T, каким бы не был T. Настоящее значение типа T используется вместо T и определяется при всяком вызове функции swapTwoValues(_:_:).

Другое различие между шаблонной и нешаблонной функциями состоит в том, что имя шаблонной функции (swapTwoValues(_:_:)) записывается вместе с замещающим именем типа (T) внутри угловых скобок (<T>. Угловые скобки говорят Swift, что T - это замещающее имя типа внутри определения функции (swapTwoValues(_:_:)). Так как T - это плейсхолдер, то Swift не ищет тип с названием T.

Функция swapTwoValues(_:_:) теперь может быть вызвана также как swapTwoInts за исключением того, что она может принять два значения любого типа, если оба этих значения имеют один и тот же тип. Всякий раз при вызове swapTwoValues(_:_:) тип T выводится из типа переданных в функцию величин.
@{18.1\1\6}
В двух примерах ниже T выводится в тип Int и String соответственно:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
Функция swapTwoValues(_:_:), определённая выше, создана на основе шаблонной функции с названием swap, которая является частью стандартной библиотеки Swift и автоматически делается доступной для использования в Ваших приложениях. Если Вам нужно поведение функции swapTwoValues(_:_:) в Вашем собственном коде, то Вы можете использовать существующую функцию swap(_:_:) вместо предоставления Вашей собственной реализации.
Параметры типов
В примере swapTwoValues(_:_:) выше плейсхолдер типа T - это пример параметра типа. Параметры типов определяют и именуют замещаемый тип и записываются сразу после имени функции между парой угловых скобок (как например ).

Как только Вы определите параметр типа, Вы можете использовать его для определения параметров функции (как например a и b в функции swapTwoValues(_:_:)) или возвращаемого значения функции или в качестве аннотации типа внутри тела функции. В любом случае, параметр типа заменяется настоящим типом при каждом вызове функции. (В примере swapTwoValues(_:_:) выше T заменяется Int при первом вызове функции и String - при втором.)

Вы можете предоставить более одного параметра типа, написав их имена в угловых скобках, разделив их запятыми.
Именование параметров типов
В большинстве случаев параметры типов имеют описательные имена, как например Key и Value в Dictionary<Key, Value> и Element в Array<Element>, которые говорят читателю о взаимоотношениях между типом параметра и шаблонным типом или функцией, в которых он используется. Однако, когда между ними нет значащего соотношения, то их традиционно называют одой буквой наподобие T, U и V, как например T в функции swapTwoValues(_:_:) выше.

Всегда давайте параметрам типов имена в стиле типов (как например T и MyTypeParameter) для указания на то, что они являются плейсхолдерами для типа, а не значения.
Шаблонные типы
В дополнение к шаблонным функциям Swift позволяет Вам определять Ваши собственные шаблонные типы. Это могут быть кастомные классы, структуры и перечисления, который могут работать с любым типом, как это делают Array и Dictionary.

Этот раздел показывает Вам, как написать шаблонный тип коллекции с названием Stack. Стэк - это сортированный набор значений, аналогичный массиве, но с более ограниченным набором операций, чем имеет тип Array в Swift. Массив позволяет новым элементам быть вставленными и удалёнными на любой позиции в массиве. Стэк, однако, позволяет новым элементам быть добавленными только к концу коллекции (известно как впихивание нового значения в стэк). Аналогично, стэк позволяет новым элементам быть удалёнными только с конца коллекции (что известно как выпихивание значения из стэка).
Как работает стэк?
  1. В стэке находится три значения.
  2. Четвёртое значение добавляется на вершину стэка.
  3. Стэк теперь содержит четыре значения, где наиболее новое находится на вершине.
  4. Вершинное значение стока извлекается.
  5. После его извлечения стэк снова содержит три значения.
Рисунок 18.1.1
@{18.1\2\1}
Здесь описано, как написать нешаблонную версию стэка для случая стэка из Int значений:
struct IntStack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}
Эта структура использует свойство типа Array с названием items для хранения значений в стэке. Stack предоставляйте два метода, push и pop, для вставки и извлечения значений в и из стэка. Эти методы маркированы как mutating, так как им нужно модифицировать (или мутировать) массив структуры items.

Тип IntStack, показанный выше, может хранить только значения Int, однако. Это было бы более полезно определить шаблонный класс Stack, который может управлять стэком любого типа значений.
@{18.1\2\2}
Здесь представлена шаблонная версия того же кода:
struct Stack {
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}
Заметьте, что шаблонная версия Stack фактически совпадает с нешаблонной, но она имеет параметр типа с названием Element вместо конкретного типа Int. Этот параметр типа записывается внутри пары угловых скобок (<Element> сразу после имени структуры.

Element определяет замещаемое имя для типа, который будет предоставлен позднее. Этот будущее тип может быть использован внутри любого места определения структуры. В этом случае Element используется в качестве плейсхолдера в трёх местах:
  • Для создания свойства с именем @code(items), которое инициализируется пустым массивом значений типа @code(Element)
  • Для спецификации метода @code(push(_:)), имеющего единственный параметр с названием @code(item), который должен иметь тип @code(Element)
  • Для определения типа возвращаемого методом @code(pop()) значения, который должен быть типом @code(Element)
Так как это шаблонный тип, то Stack может быть использован для создания стока любого корректного типа в Swift, как это происходит с типами Array и Dictionary.
@{18.1\2\3}
Вы можете теперь создать новый объект типа Stack, написав тип хранимого в стэке/ внутри угловых скобок. Например, для создания нового стэка/ строк, Вы можете написать Stack<String>():
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
Рисунок 18.1.2
Здесь показано, как выглядит stackOfStrings после вставки этих четырёх значений в него:
@{18.1\2\4}
Извлечение значения из стэка удаляет и возвращает вершинное значение, "cuatro":
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
Рисунок 18.1.3
Вот как Выглядит стэк после извлечения его вершинного значения:
Расширение шаблонного типа
Когда Вы расширяете шаблонный тип, Вам не надо предоставлять список параметров типа в качестве определения расширения. Вместо этого список параметров типа из оригинального определения доступен изнутри тела расширения, и оригинальные параметры типов используется для ссылки на параметры типов из оригинального определения.
@{18.1\2\5}
Следующий пример расширяет шаблонный тип Stack для добавления вычисляемого свойства только-для-чтения с названием topItem, которое возвращает вершинное значение на стэке без его извлечения:
extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}
Свойство topItem возвращает опциональное значение типа Element. Если стэк пусть, то topItem вернёт nil, если стэк не пуст, то topItem вернёт последний элемент из массива array.

Заметьте, что это расширение не определяет списка параметров типа. Вместо этого уже имеющееся у типа Stack имя параметра Element используется внутри расширения для указания на опциональный тип вычисляемого свойства topItem.
@{18.1\2\6}
Вычисляемое свойство topItem теперь может быть использовано у любого экземпляра типа Stack для доступа и запроса на его вершинный элемент без его изъятия.
if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Выведет "The top item on the stack is tres."
Расширения шаблонного типа могут также включать требования на то, что объекты расширяемого типа должны им удовлетворять для того, чтобы получить новую функциональность.