Перейти к содержимому
Теория · Протоколы и обобщённый код · урок 8 из 9

Обобщённые функции и типы

Вам нужна функция, которая находит самое большое число в массиве. Потом нужна ещё самая «большая» строка, то есть последняя по алфавиту. Код у двух функций совпадает до буквы, разный только тип: [Int] в одной и [String] в другой. Копировать функцию под каждый тип неудобно: исправление в одной копии придётся повторять во всех остальных.

Представьте станок с регулируемым гнездом. Перед работой гнездо подгоняют под деталь, и станок обрабатывает болт, гайку или шайбу одними и теми же движениями. На станке висит табличка с требованием к детали, например «Только металл». Деталь, которая требованию не отвечает, в гнездо не встанет.

Функцию или тип с таким гнездом называют обобщёнными: тип значений в них не записан заранее, а подставляется при использовании.

Обобщённая функция

Напишем одну функцию для чисел, строк и любых других значений, которые можно сравнивать:

SWIFT
func largest<T: Comparable>(_ items: [T]) -> T? {
    if items.isEmpty {
        return nil
    }
    var best = items[0]
    for item in items {
        if item > best {
            best = item
        }
    }
    return best
}

print(largest([3, 9, 4]) ?? 0) // 9
print(largest(["pear", "apple"]) ?? "none") // pear

Разберём объявление по частям:

  1. <T: Comparable> после имени функции — гнездо. T — параметр типа: имя, вместо которого при каждом вызове встанет настоящий тип.
  2. : Comparable — табличка на станке, или ограничение: в гнездо подходит только тип, который соответствует Comparable.
  3. _ items: [T] и -> T? — на входе массив значений этого типа, а на выходе одно такое значение или nil, если массив пуст.
  4. При вызове largest([3, 9, 4]) Swift видит массив Int и подставляет Int вместо T. Для ["pear", "apple"] он подставляет String.

Тип результата не теряется: largest([3, 9, 4]) возвращает Int?, и ?? 0 подставляет число.

Здесь картинка со станком неточна. Станок перенастраивают руками, а обобщённую функцию никто не настраивает: Swift сам подбирает T по аргументам каждого вызова.

Зачем нужно ограничение

Внутри largest есть сравнение item > best. Оператор > есть не у каждого типа, а только у Comparable, и ограничение обещает компилятору, что у T этот оператор будет. Компилятор следит за этим обещанием с двух сторон:

  • тело функции может пользоваться только тем, что даёт ограничение;
  • вызов с типом без Comparable не скомпилируется.

Если передать в largest массив структур Point без Comparable, компилятор остановит вызов:

TEXT
error: global function 'largest' requires that 'Point' conform to 'Comparable'

Обобщённый тип

Гнездо бывает и у типов. Стопка тарелок устроена одинаково, какие бы тарелки в ней ни лежали: тарелку кладут сверху и сверху же берут.

SWIFT
struct Stack<Element> {
    var items: [Element] = []
    var top: Element? { items.last }
    mutating func push(_ item: Element) {
        items.append(item)
    }
}

var plates = Stack<String>()
plates.push("blue")
plates.push("red")
print(plates.top ?? "empty", plates.items.count) // red 2

Что здесь важно:

  • Element — параметр типа, как T. Имя можно выбрать любое, лишь бы оно читалось.
  • Stack<String>() указывает тип в угловых скобках, и push принимает только строки.
  • Ограничения здесь нет: стопке не нужно сравнивать тарелки, поэтому в неё можно класть значения любого типа.

Обобщёнными типами вы уже пользовались. [Int] — короткая запись Array<Int>, а Set<Int> — полный тип множества чисел.

Частая ошибка

Ограничение забыли:

SWIFT
func larger<T>(_ a: T, _ b: T) -> T {
    a > b ? a : b // ✗
}
TEXT
error: referencing operator function '>' on 'Comparable' requires that 'T' conform to 'Comparable'

Программа не компилируется, даже если вызывать larger только с числами. Компилятор проверяет тело один раз и для любого T, а не для типов из конкретных вызовов. Умеет ли любой T сравниваться через >, неизвестно, поэтому сравнение запрещено.

Этот вопрос часто задают на собеседовании: почему обобщённая функция без ограничения не может сравнить свои аргументы? Ответ: про T без ограничения компилятор не знает ничего, кроме того, что это тип. Исправление — <T: Comparable>.

Главное

  • Обобщённая функция или тип объявляет параметр типа в угловых скобках, <T>, а Swift подставляет настоящий тип при каждом использовании.
  • Ограничение <T: Comparable> пускает в функцию только типы с этим протоколом и разрешает телу сравнивать значения.
  • Без ограничения a > b в теле не скомпилируется, даже если функцию вызывают только с Int.
  • Stack<Element>, Array<Int> и Set<Int> — обобщённые типы: одно устройство для элементов любого типа.