Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
191: Associated Types vs Generic Parameters
You've probably run into this while digging through the standard library: some traits look like Iterator<Item = T>, while others look like From<T>. On the surface, they both seem to be doing the same thing—letting the trait work with different types. But in practice, they serve two completely different architectural purposes.
Wait, why bother with associated types when I can just use a generic parameter?
The short answer is: associated types simplify your function signatures.
Imagine we're building a Graph library. If we used a generic parameter for the node type, the trait would look like this:
trait Graph<N> {
fn add_node(&mut self, node: N);
fn get_nodes(&self) -> Vec<N>;
}
Now, imagine you write a function that takes any graph and prints the nodes. Your signature becomes: fn print_nodes<G: Graph<N>, N>(graph: G). Notice how I had to introduce N as a generic parameter for the function itself, even though I don't actually care what N is? I just care that the graph knows what its own node type is.
If we use an associated type, it looks like this:
trait Graph {
type Node;
fn add_node(&mut self, node: Self::Node);
fn get_nodes(&self) -> Vec<Self::Node>;
}
Now my function is just: fn print_nodes<G: Graph>(graph: G). The compiler knows that if G implements Graph, it must have exactly one Node type associated with it. I don't have to drag extra generic parameters around my entire codebase like a heavy suitcase.
Can I actually implement a trait multiple times if I use generics?
Yes, and that's exactly why generics exist in traits. This is the "killer feature" that associated types can't do.
With an associated type, you can only implement the trait once for a specific type. If MyGraph implements Graph with type Node = String, you cannot also implement Graph for MyGraph with type Node = i32. It's a 1:1 relationship.
But with generics, it's 1:Many. Look at the From<T> trait. I can implement From<u32> for my UserId struct, and I can also implement From<String> for that same UserId struct.
struct UserId(u32);
impl From<u32> for UserId {
fn from(val: u32) -> Self { UserId(val) }
}
impl From<String> for UserId {
fn from(val: String) -> Self {
// some parsing logic here
UserId(val.parse().unwrap())
}
}
If From used an associated type, you'd have to pick one single type that UserId could be created from, and that's just not how real-world data works.
How does this change how the compiler figures out types?
This is where the "magic" (and the frustration) happens. When you use an associated type, the implementing type determines the associated type. If I have a Vec<i32>, and it implements Iterator, the compiler knows immediately that the Item is i32.
With generics, the caller often determines the type. If I have a function fn convert<T, U>(input: T) where U: From<T>, the compiler can't know what U is just by looking at input. You usually have to provide a type hint, like let x: MyType = convert(input);.
I generally follow this rule of thumb: If the type is a "property" of the implementation (like the Item of an Iterator), use an associated type. If the type is an "input" or "target" that can vary (like the source type of a conversion), use a generic parameter.
📋 Practical Task
Implementing a Multi-Format Document Exporter
You are building a document system where a Document can be exported into multiple different formats (e.g., PDF, HTML, Markdown). Because a single document should be exportable to many different formats, you cannot use associated types for the format—you must use generic parameters.
Your Task:
- Create a trait called
Exportable<F>whereFrepresents the output format. It should have one method:fn export(&self, format: F) -> String; - Create a struct
Reportthat holds atitle: Stringandcontent: String. - Create two empty structs:
PdfFormatandHtmlFormat. - Implement
Exportable<PdfFormat>forReport, returning a string that starts with "[PDF] ". - Implement
Exportable<HtmlFormat>forReport, returning a string that starts with "[HTML] ". - In your
mainfunction, instantiate aReportand callexportusing both format types to prove that one struct can implement the trait multiple times.
There are no comments for now.