type-level выход трейтаa trait's type-level output

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

Ассоциированный тип — заполнитель типа, объявленный внутри определения трейта и указываемый один раз на каждую реализацию, например trait Container { type Item; fn get(&self, i: usize) -> Self::Item; }. Классический пример из стандартной библиотеки — сам трейт Iterator: trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }.An associated type is a placeholder type declared inside a trait definition and specified once per implementation, e.g. trait Container { type Item; fn get(&self, i: usize) -> Self::Item; }. The classic real-world example is the standard library's own Iterator trait: trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }.

ровно один естественный тип на реализациюexactly one natural type per implementation частый вопрос: ассоциированный тип vs обобщённый параметр трейтаcommon question: associated type vs. a generic trait parameter

Что этоWhat it is

Каждый тип, реализующий Iterator, указывает РОВНО ОДИН конкретный Item (например, impl Iterator for Counter { type Item = u32; ... }) — в отличие от обобщённого параметра трейта (trait Container<Item> { ... }), который позволил бы одному и тому же типу реализовать Container<i32> И Container<String> одновременно.Every type implementing Iterator specifies exactly ONE concrete Item (e.g. impl Iterator for Counter { type Item = u32; ... }) — unlike a generic trait parameter (trait Container<Item> { ... }), which would let the same type implement Container<i32> AND Container<String> at once.

обобщённый параметр трейта — можно реализовать многократноgeneric trait parameter — can be implemented many times
trait Container<T> {
    fn get(&self) -> T;
}
// один тип может реализовать Container<i32> и Container<String> одновременно// one type can implement both Container<i32> and Container<String> at once
impl Container<i32> for Wrapper { fn get(&self) -> i32 { 0 } }
impl Container<String> for Wrapper { fn get(&self) -> String { String::new() } }
ассоциированный тип — ровно одна реализацияassociated type — exactly one implementation
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}
// Counter может реализовать Iterator ровно один раз, с одним фиксированным Item// Counter can implement Iterator exactly once, with one fixed Item
impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> { /* ... *//* ... */ None }
}

Зачем ввелиWhy it was introduced

Ассоциированные типы отвечают на реальный проектный вопрос: должен ли тип иметь право реализовать трейт многократно с разными типовыми параметрами, или для трейта осмысленна ровно одна реализация на тип?Associated types answer a real design question: should a type be allowed to implement a trait multiple times with different type parameters, or does exactly one implementation per type make sense for this trait?

01

Множественные реализации Iterator сделали бы for-цикл неоднозначнымMultiple Iterator implementations would make a for-loop ambiguous

Если бы один тип мог одновременно быть Iterator<Item = i32> И Iterator<Item = String>, выражение for x in my_iterator стало бы неоднозначным — какого типа x? Ассоциированный тип напрямую выражает в системе типов "у этого трейта ровно один естественный выходной тип на реализацию".If a single type could simultaneously be Iterator<Item = i32> AND Iterator<Item = String>, the expression for x in my_iterator would become genuinely ambiguous — what type is x? An associated type directly expresses in the type system "this trait has exactly one natural output type per implementor".

02

Документирует замысел проектированияDocuments design intent

Использование ассоциированного типа вместо обобщённого параметра трейта — явное заявление автора трейта: "здесь предполагается один естественный тип на реализующий тип", а не "тип может параметризоваться этим трейтом произвольным числом способов".Using an associated type instead of a generic trait parameter is an explicit statement by the trait's author: "there is one natural type per implementing type here", not "a type may be parametrized by this trait in arbitrarily many ways".

03

Код вызывающей стороны короче, ошибки яснееShorter calling code, clearer errors

На месте использования ограничения пишется просто Iterator, а не Iterator<Item = T>, когда T уже определён контекстом — практический выигрыш в лаконичности и в качестве сообщений об ошибках.At a bound site you write plain Iterator instead of Iterator<Item = T> when T is already determined by context — a practical win for brevity and error-message clarity.

Как это работает под капотомHow it works under the hood

Реальная форма трейта Iterator объявляет type Item; и метод, возвращающий Option<Self::Item>. Реализация для конкретного типа фиксирует этот тип один раз:The real shape of the Iterator trait declares type Item; and a method returning Option<Self::Item>. An implementation for a concrete type fixes that type exactly once:

struct Counter { count: u32 }

impl Iterator for Counter {
    type Item = u32; // зафиксировано один раз для Counter// fixed once, for Counter

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Гипотетическая версия того же трейта с обобщённым параметром, trait Container<T> { fn get(&self) -> T; }, допустила бы для одной структуры несколько одновременных реализаций — Container<i32> и Container<String> для одного и того же типа сразу, тогда как Iterator с фиксированным Item можно реализовать только один раз.A hypothetical version of the same trait with a generic parameter, trait Container<T> { fn get(&self) -> T; }, would allow several simultaneous implementations for one struct — Container<i32> and Container<String> for the same type at once, whereas Iterator with a fixed Item can only be implemented once.

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Реализация Iterator для собственного CounterImplementing Iterator for a custom Counter

Реализовав настоящий трейт Iterator с type Item = u32, тип автоматически становится пригодным для for-цикла и всех адаптеров итераторов (см. iterators.html).By implementing the real Iterator trait with type Item = u32, a type automatically becomes usable in a for loop and with all iterator adaptors (see iterators.html).

struct Counter { count: u32 }

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> {
        if self.count < 5 { self.count += 1; Some(self.count) } else { None }
    }
}

for n in (Counter { count: 0 }) {
    println!("{}", n); // 1, 2, 3, 4, 5// 1, 2, 3, 4, 5
}
02

Graph с ассоциированными типами Node и EdgeA Graph trait with associated Node and Edge types

Обобщённые алгоритмы над графами не обязаны отдельно параметризоваться по типу узла и типу ребра в каждой сигнатуре — оба типа выводятся из одного bound G: Graph.Generic graph algorithms don't need to separately parametrize by node type and edge type in every signature — both types are inferred from a single G: Graph bound.

trait Graph {
    type Node;
    type Edge;
    fn edges(&self, node: &Self::Node) -> Vec<Self::Edge>;
}

fn count_edges<G: Graph>(g: &G, node: &G::Node) -> usize {
    g.edges(node).len() // работает для любого конкретного представления графа// works for any concrete graph representation
}
03

Self::Item в теле default-методаSelf::Item inside a default method body

Внутри трейта можно ссылаться на собственный ассоциированный тип через Self::Item, не зная заранее, каким он будет для конкретной реализации.Inside a trait, you can refer back to its own associated type via Self::Item without knowing in advance what it will be for a given implementation.

trait PeekTwo: Iterator {
    fn collect_first_two(&mut self) -> Vec<Self::Item> {
        let mut out = Vec::new();
        if let Some(a) = self.next() { out.push(a); }
        if let Some(b) = self.next() { out.push(b); }
        out // Self::Item — тот же ассоциированный тип, что и у базового Iterator// Self::Item is the same associated type as the underlying Iterator's
    }
}
04

std::ops::Add и его ассоциированный Outputstd::ops::Add and its associated Output

Add сам имеет ассоциированный тип type Output; — он не обязан совпадать с Self. Здесь для простоты Output = Self, но в принципе ничто не мешает, скажем, гипотетическому сложению двух Vector2 давать Vector3 в другой предметной области — гибкость ассоциированного типа это допускает.Add itself has an associated type type Output; — it need not match Self. Here Output = Self for simplicity, but in principle nothing stops, say, a hypothetical addition of two Vector2 values from producing a Vector3 in a different domain — the associated type's flexibility allows exactly that.

use std::ops::Add;

struct Point { x: i32, y: i32 }

impl Add for Point {
    type Output = Self; // здесь Output = Self, но это не обязательное правило// Output = Self here, but that's not a required rule
    fn add(self, other: Self) -> Self {
        Point { x: self.x + other.x, y: self.y + other.y }
    }
}

ГраблиGotchas

Ассоциированный тип и обобщённый параметр решают разные задачиAssociated types and generic parameters solve different problems

Правило простое: используйте ассоциированный тип, когда у типа должна быть ровно ОДНА реализация трейта (есть один "естественный" ответ); используйте обобщённый параметр, когда тип разумно может реализовать трейт несколько раз с разными типовыми аргументами. Ошибочный выбор ведёт к неудобному API.The rule of thumb: use an associated type when a type should have exactly ONE implementation of the trait (there's one "natural" answer); use a generic parameter when a type might reasonably implement the trait multiple times for different type arguments. Choosing wrong leads to an awkward API.

Неоднозначность требует полностью квалифицированного синтаксисаAmbiguity requires fully qualified syntax

Если в области видимости несколько трейтов объявляют ассоциированный тип с одинаковым именем, ссылка на него извне определения трейта требует <Type as Trait>::Item — реальная, хоть и нечастая, синтаксическая тонкость.If multiple in-scope traits define an associated type with the same name, referring to it from outside the trait's own definition requires <Type as Trait>::Item — a real, if uncommon, syntactic wrinkle.

Значения по умолчанию для ассоциированных типов — не то же самое, что default у дженериковDefault associated types are not quite like generic defaults

Для обобщённого параметра синтаксис trait Foo<T = i32> легален и хорошо понятен. У ассоциированных типов тоже есть синтаксис значения по умолчанию (type Item = i32; прямо в определении трейта) в стабильном Rust, но он используется заметно реже и имеет ряд ограничений — стоит считать это менее устоявшейся возможностью, а не автоматически ожидать полной идентичности поведения с дефолтами дженериков.For a generic parameter, trait Foo<T = i32> is legal and well understood. Associated types also have default-value syntax (type Item = i32; right in the trait definition) on stable Rust, but it's used noticeably less often and carries some restrictions — treat it as a less settled feature rather than assuming it behaves identically to generic defaults.