On this page
Associated Types and GATs
Associated types (
type Item) allow each implementation of a trait to have only oneItemtype, complementing generic parameters (where each implementation can have infiniteTs)—associated types enforce uniqueness, while generic parameters allow diversity. GATs (Generic Associated Types, 1.65+) allow associated types themselves to carry generic parameters, unlocking abstractions that were previously impossible to express, such as traits that return borrowed data from themselves and generic iterators.
Associated Types: One Concrete Type per impl
Why Not Use Generic Parameters?
You certainly could write trait Iterator<T> { fn next(&mut self) -> Option<T>; }. However, this would mean that Counter could simultaneously implement Iterator<i32>, Iterator<String>, and Iterator<f64>—one independent impl for each T. This doesn't make sense for Iterator—a type should only produce one kind of item.
Associated types express a "one-to-one" semantic constraint: at most one impl per type. Generic parameters express a "one-to-many" relationship: a type can have multiple impls for different generic parameters.
This is not just a matter of semantic preference—it affects type inference. When using associated types, Iterator::Item can be uniquely determined from Self. When using generic parameters, the compiler must infer T from the context. Associated types make the compiler's job simpler and the API clearer.
GATs (Generic Associated Types): Associated Types with Lifetimes
Rust 1.65 introduced GATs—associated types can now have their own lifetime or generic parameters:
What problem do GATs solve? The traditional Iterator trait cannot express "returning a reference that borrows from self." You cannot write type Item = &[u8] because references must have a lifetime—and there was nowhere to write a lifetime on an associated type. GATs' type Item<'a> allows associated types to use lifetimes, making Item<'a> = &'a [u8] possible—the returned reference is tied to the lifetime of &self.
This is known as a lending iterator—the iterator "lends" data rather than transferring ownership. The standard library's Iterator is not lending (next returns Option<Self::Item>, it does not borrow &mut self), so you cannot hold a reference to the iterator's internals on the elements returned by the iterator. GATs remove this limitation.
GATs are also the foundation for async traits. The Future type generated by async fn is anonymous and cannot be expressed directly in a trait. GATs allow forms like type Future<'a>: Future<Output = X>—a trait can have a method that returns a Future without the heap allocation overhead of Box<dyn Future>.
impl Trait in Return Position
The caller does not know the concrete return type—they only know that "it implements Iterator<Item = i32>." The compiler knows, so it can perform static dispatch and inlining. The difference from Box<dyn Iterator>: impl Trait uses static dispatch (zero overhead, no virtual function calls), but the caller cannot assign functions with different return types to each other—this is the cost of the compiler's abstraction boundary.
References
- RFC 1598: Generic Associated Types
- Rust Blog: "GATs are stable!" (2022)
- Rust Book: Chapter 19.4
Keywords: associated type, GAT, lending iterator, impl Trait, static dispatch, type inference