Ассоциированные типы в 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, остальные разбираются в тренажёре.
- Как в сигнатуре потребовать итератор именно по строкам?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), а не тип элемента.
- Зачем нужны GAT (generic associated types)?A)Чтобы объявлять ассоциированные типы у дженерик-структурB)Чтобы один трейт можно было реализовать для типа несколько разC)Чтобы ассоциированные типы работали в трейт-объектахD)Чтобы ассоциированный тип сам принимал параметры и лайфтайм
показать ответ и разбор
+D)Чтобы ассоциированный тип сам принимал параметры и лайфтайм// разбор: GAT разрешают писать type Item<'a> и тем самым выражать вещи вроде лендинг-итератора, который отдаёт ссылки на собственный буфер: каждый следующий элемент связан лайфтаймом с заимствованием self. До стабилизации в 1.65 такие API приходилось городить через обходные трейты или клонирование.
- Что означает ограничение where T: Iterator, T::Item: Display?A)T — итератор, и его элементы умеют форматироваться для выводаB)Элементы T приводятся к строке автоматически при печатиC)T реализует и Iterator, и Display одновременноD)T — итератор по строкам, других элементов быть не может
показать ответ и разбор
+A)T — итератор, и его элементы умеют форматироваться для вывода// разбор: Ограничения вешают не только на сам параметр, но и на его ассоциированные типы: тело функции сможет печатать элементы через {}. Это ровно то, ради чего существует where — в угловых скобках такая запись быстро становится нечитаемой, а через where ограничения выстраиваются в список.
- Что такое ассоциированный тип в трейте?A)Псевдоним для типа реализации, который выбирает вызывающий код в момент обращения к методуB)Обёртка над параметром типа, позволяющая реализовать трейт для типа несколько раз подрядC)Тип, подставляемый компилятором автоматически по типу первого аргумента методаD)Тип, который задаёт сама реализация трейта и который дальше доступен как T::Имя
показать ответ и разбор
+D)Тип, который задаёт сама реализация трейта и который дальше доступен как T::Имя// разбор: Это часть контракта, которую заполняет реализация: у вектора строк итератор отдаёт String, и второго варианта у него не бывает. Вызывающий на выбор не влияет — он лишь может уточнить тип в границе, написав Iterator<Item = String>. Тем и отличается от параметра типа: параметр выбирает пользователь, ассоциированный тип — автор реализации.
- Как в коде обратиться к ассоциированному типу, если у типа несколько реализаций разных трейтов с одинаковым именем типа?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). Это тот же приём, что и турбофиш, только для трейтов, а не для параметров типа.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.