сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Трейты и дженерики

Ассоциированные типы в Rust

Ассоциированные типы

Вопрос, который отделяет тех, кто писал свои абстракции, от тех, кто только пользовался чужими. Спрашивают, почему у Iterator тип элемента ассоциированный, а у From - параметр, и что будет при попытке реализовать трейт дважды.

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

// Формулировки: «почему Iterator::Item, а не Iterator<T>?», «можно ли реализовать Iterator дважды?», «зачем GAT?»

Кто выбирает тип

У Iterator тип элемента определяется самой коллекцией: вектор строк итерируется строками, и второго варианта быть не может. Такой тип делают ассоциированным - он часть реализации, и писать его в каждом использовании не нужно.

У From всё наоборот: один тип может конвертироваться из многих других, поэтому там параметр - From<u8>, From<u32>, сколько угодно реализаций для одного типа. Правило запоминается так: если у типа может быть только один разумный вариант - ассоциированный тип; если несколько равноправных - параметр.

// Попытка реализовать Iterator дважды с разными Item даёт E0119, conflicting implementations: ассоциированный тип не входит в идентичность реализации.

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> { .. }
}
ассоциированный тип
тип, который задаёт реализация трейта
E0119
конфликт реализаций трейта для одного типа

Как их уточнять в сигнатурах

Ассоциированный тип уточняют равенством в угловых скобках: I: Iterator<Item = String>. Запись Iterator<String> - ошибка, у Iterator нет параметра типа. Через плюс добавляют независимые трейты: I: Iterator + Clone.

Ограничения можно вешать и на сам ассоциированный тип: where I: Iterator, I::Item: Display. Это тот случай, когда where заметно выигрывает у угловых скобок по читаемости.

// GAT (generic associated types) (стабилизированы в 1.65) идут дальше: сам ассоциированный тип получает параметры, включая лайфтайм - type Item<'a>. Так выражают лендинг-итератор, который отдаёт ссылки на собственный буфер; обычным Iterator это не записывается.

уточнение Item =
фиксация ассоциированного типа в ограничении
GAT
ассоциированный тип с собственными параметрами

Как отвечать: «Почему у Iterator тип элемента ассоциированный, а не параметр?»

Потому что его выбирает реализация, а не вызывающий. У конкретной коллекции ровно один разумный тип элемента, и позволить реализовать Iterator для неё дважды с разными Item было бы бессмысленно - компилятор и не позволяет, это конфликт реализаций. Ассоциированный тип это и выражает: он часть контракта реализации, поэтому в сигнатурах его не приходится таскать. Когда вариантов действительно несколько равноправных, берут параметр - как у From, который у одного типа реализуют и для u8, и для u32.

Ты формулируешь критерий выбора, а не пересказываешь пример. Именно этот критерий и нужен, когда проектируешь свой трейт.

На чём валятся

  • Пишут Iterator<String> вместо Iterator<Item = String>.
  • Считают, что разные Item дадут две реализации Iterator для одного типа.
  • Не могут сформулировать критерий выбора между ассоциированным типом и параметром.
  • Вешают ограничения только на сам параметр и не знают, что их можно вешать на I::Item.
  • Не слышали про GAT и не могут объяснить, почему лендинг-итератор не выражается обычным Iterator.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_assoc_types1 / 5
    Как в сигнатуре потребовать итератор именно по строкам?
    A)fn f<I: Iterator, String>(it: I)
    B)fn f<I: Iterator<Item = String>>(it: I)
    C)fn f<I: Iterator<String>>(it: I)
    D)fn f<I: Iterator + String>(it: I)
    показать ответ и разбор
    +B)fn f<I: Iterator<Item = String>>(it: I)

    // разбор: Ассоциированный тип уточняют в угловых скобках через равенство: Iterator<Item = String>. Синтаксис Iterator<String> подошёл бы для трейта с параметром, а у Iterator параметра нет. Плюс через + добавляют независимые ограничения (Iterator + Clone), а не тип элемента.

  2. #rs_assoc_types2 / 5
    Зачем нужны GAT (generic associated types)?
    A)Чтобы объявлять ассоциированные типы у дженерик-структур
    B)Чтобы один трейт можно было реализовать для типа несколько раз
    C)Чтобы ассоциированные типы работали в трейт-объектах
    D)Чтобы ассоциированный тип сам принимал параметры и лайфтайм
    показать ответ и разбор
    +D)Чтобы ассоциированный тип сам принимал параметры и лайфтайм

    // разбор: GAT разрешают писать type Item<'a> и тем самым выражать вещи вроде лендинг-итератора, который отдаёт ссылки на собственный буфер: каждый следующий элемент связан лайфтаймом с заимствованием self. До стабилизации в 1.65 такие API приходилось городить через обходные трейты или клонирование.

  3. #rs_assoc_types3 / 5
    Что означает ограничение where T: Iterator, T::Item: Display?
    A)T — итератор, и его элементы умеют форматироваться для вывода
    B)Элементы T приводятся к строке автоматически при печати
    C)T реализует и Iterator, и Display одновременно
    D)T — итератор по строкам, других элементов быть не может
    показать ответ и разбор
    +A)T — итератор, и его элементы умеют форматироваться для вывода

    // разбор: Ограничения вешают не только на сам параметр, но и на его ассоциированные типы: тело функции сможет печатать элементы через {}. Это ровно то, ради чего существует where — в угловых скобках такая запись быстро становится нечитаемой, а через where ограничения выстраиваются в список.

  4. #rs_assoc_types4 / 5
    Что такое ассоциированный тип в трейте?
    A)Псевдоним для типа реализации, который выбирает вызывающий код в момент обращения к методу
    B)Обёртка над параметром типа, позволяющая реализовать трейт для типа несколько раз подряд
    C)Тип, подставляемый компилятором автоматически по типу первого аргумента метода
    D)Тип, который задаёт сама реализация трейта и который дальше доступен как T::Имя
    показать ответ и разбор
    +D)Тип, который задаёт сама реализация трейта и который дальше доступен как T::Имя

    // разбор: Это часть контракта, которую заполняет реализация: у вектора строк итератор отдаёт String, и второго варианта у него не бывает. Вызывающий на выбор не влияет — он лишь может уточнить тип в границе, написав Iterator<Item = String>. Тем и отличается от параметра типа: параметр выбирает пользователь, ассоциированный тип — автор реализации.

  5. #rs_assoc_types5 / 5
    Как в коде обратиться к ассоциированному типу, если у типа несколько реализаций разных трейтов с одинаковым именем типа?
    A)Через полную форму: <T as Iterator>::Item — она снимает неоднозначность
    B)Через T::Item — компилятор выберет реализацию по контексту использования значения
    C)Через Iterator::Item::<T> — параметр указывается турбофишем после имени типа
    D)Никак: имена ассоциированных типов уникальны в пределах крейта по правилам согласованности
    показать ответ и разбор
    +A)Через полную форму: <T as Iterator>::Item — она снимает неоднозначность

    // разбор: Короткая запись T::Item работает, пока подходящая реализация одна. Если их несколько, компилятор просит уточнить, и уточняют полной формой — <T as Trait>::Item. Та же форма нужна для вызова конкретного метода при конфликте имён: <S as Greet>::hi(&s). Это тот же приём, что и турбофиш, только для трейтов, а не для параметров типа.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.