Зачем дженерики
Быстро встречаются типы, которые должны работать более чем с одним типом элемента. «Пара» из двух значений не возражает, целые там, строки или какой-то ваш shape. Дженерики позволяют написать тип один раз и инстанцировать его теми типами элементов, которые нужны вызывающему.
Альтернатива — писать IntPair, StringPair, BytePair и так далее — быстро становится скучной и плохо композируется. Дженерики в Zero — стандартный инструмент для этой задачи.
Дженерик-функции
Объявите параметры типа в угловых скобках между именем функции и списком параметров:
fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
return Pair { left: left, right: right }
}
T и U — это placeholder-типы; вызывающий решает, чем они станут.
Вызовы обычно не требуют их прописывания — компилятор выводит из типов аргументов:
let pair = makePair(40, 2_u8)
Здесь T выводится как i32 (по умолчанию для целочисленного литерала без суффикса), а U — как u8 (из суффикса _u8). Результирующая привязка pair имеет тип Pair<i32, u8>.
Если вывод выбрал бы неправильные типы — например, потому что литералы неоднозначны — параметры можно закрепить прямо в месте вызова:
let pair = makePair<u8, u8>(1, 2)
(Точный синтаксис вызова с угловыми скобками может меняться между версиями Zero — посмотрите актуальную документацию. Поведение «сначала вывод» — это стабильная часть.)
Дженерик-shapes
Shapes принимают параметры типа точно так же:
shape Pair<T, U> {
left: T,
right: U,
}
В типе каждого поля можно упоминать параметры. Экземпляры их фиксируют:
let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words: Pair<String, String> = Pair { left: "hi", right: "there" }
Разобранный пример с дженерик-shape и дженерик-функцией — нажмите Run и увидите вывод типов в действии:
Одно объявление дженерик-shape, одна дженерик-функция, один строго типизированный сайт вызова. Никакого boilerplate в духе IntBytePair.
Type aliases
Когда одна и та же параметризация всплывает снова и снова, дайте ей имя через type:
type BytePair = Pair<u8, u8>
Теперь BytePair взаимозаменяем с Pair<u8, u8> везде, где можно написать тип:
Псевдоним — это чисто именование, новый тип он не создаёт. Функция, принимающая BytePair, спокойно примет значение типа Pair<u8, u8> (и наоборот).
Дженерики в стандартной библиотеке
Тот же механизм питает большую часть стандартной библиотеки. Несколько из тех, что встретятся в реальном Zero-коде:
Maybe<T>— опциональное значение, содержит либоT, либо ничего.Span<T>— заимствованный срез поT.Span<u8>— каноническое представление байтового буфера.ref<T>иmutref<T>— явные типы ссылок для случаев, когда нужно делиться данными без копирования.
Учить их все сразу не надо. Смысл дженериков в том, что один и тот же shape работает с любым типом элемента, который у вас под рукой.
Когда дженерики окупаются (и когда нет)
Тянитесь к дженерикам, когда ловите себя на том, что пишете одну и ту же функцию или shape дважды с разными типами элементов. Тянитесь к конкретному типу, когда:
- Логика функции имеет смысл только для конкретного типа (парсер
String, скажем). - Хочется, чтобы тип появлялся в сообщениях об ошибках, чтобы упростить отладку.
- Характеристики производительности зависят от конкретного размера в памяти.
Цена дженериков реальна — бинарники больше (каждая инстанциация порождает новый код) и компиляция чуть медленнее. Для большинства прикладного кода эта цена пренебрежимо мала, но об этом полезно знать, когда вы пишете плотный код в embedded-стиле, где размер бинарника важен.
Заметка про ограничения
В некоторых системах дженериков параметр типа можно ограничить («T должен поддерживать ==», «T должен реализовывать Iterator»). В Zero pre-1.0 история с ограничениями ещё эволюционирует — примеры в официальном репозитории используют дженерики в обычной форме, без хитрых границ. По мере стабилизации языка ожидайте, что синтаксис ограничений появится в маленькой регулярной форме, согласованной с остальным языком. Пока пишите дженерики, которые работают с любым T, который вы реально в них передаёте, и доверяйте компилятору сообщать, когда какая-то операция не поддерживается.
Дальше: Enums
Дженерики позволяют параметризоваться по типам. Следующий строительный блок — на другом конце спектра: enums, простой тип перечисления Zero для случаев, когда варианты не несут дополнительных данных.
Часто задаваемые вопросы
Как работают дженерики в Zero?
Объявите параметры типа в угловых скобках после имени функции или shape: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> или shape Pair<T, U> { left: T, right: U }. Вызывающие либо явно закрепляют параметры (Pair<i32, u8>), либо позволяют компилятору вывести их из аргументов вызова.
Могут ли shapes быть дженериками в Zero?
Да. Shape может принимать параметры типа в том же синтаксисе с угловыми скобками, что и функции: shape Pair<T, U> { left: T, right: U }. Каждое поле может использовать параметры в своём типе. Экземпляры формируются написанием параметризованного типа — например, Pair<i32, u8>.
Нужно ли указывать параметры типа при вызове дженерик-функции?
Обычно нет. Компилятор выводит их из типов аргументов. Вызова makePair(40, 2_u8) достаточно — T становится i32, а U — u8. Закрепить параметры явно стоит, когда вывод выбрал бы не тот тип или когда хочется задокументировать их в месте вызова.
Что такое type alias в Zero?
Type alias — это короткое имя для более длинного выражения типа. type BytePair = Pair<u8, u8> позволяет писать BytePair везде, где иначе пришлось бы писать Pair<u8, u8>. Псевдоним — чисто именовательная штука: он не вводит новый тип, а только короткий способ сослаться на существующий.
Где дженерики встречаются в стандартной библиотеке Zero?
Везде — везде, где типу нужно держать или работать с произвольным типом элемента. Maybe<T> для опциональных значений, Span<u8> для байтовых срезов, контейнеры, параметризованные типом элемента. Один и тот же механизм дженериков обрабатывает и пользовательские типы, и типы стандартной библиотеки.