09 - Traits and Generics¶
What this session is¶
About an hour. You'll learn traits (Rust's interface system) and generics (type parameters). Together they give Rust most of what other languages get from inheritance, but more flexibly.
The problem traits solve¶
In object-oriented languages you'd say "Cat IS-A Animal, Dog IS-A Animal, both have a speak() method." Rust doesn't have inheritance. Instead, it has traits - shared behavior defined as a contract.
trait Animal {
fn speak(&self) -> String;
}
struct Cat { name: String }
struct Dog { name: String }
impl Animal for Cat {
fn speak(&self) -> String {
format!("{} says meow", self.name)
}
}
impl Animal for Dog {
fn speak(&self) -> String {
format!("{} says woof", self.name)
}
}
fn main() {
let c = Cat { name: String::from("Whiskers") };
let d = Dog { name: String::from("Rex") };
println!("{}", c.speak());
println!("{}", d.speak());
}
What's new:
- trait Animal { ... } declares the trait - a set of methods (with signatures, optionally with default bodies).
- impl Animal for Cat { ... } provides the methods for Cat. Read as "implement Animal for Cat."
- Any type with the Animal impl can be treated as an Animal.
You can have multiple traits for the same type (a struct can implement many traits). You can have one trait implemented for many types. This combinatorial freedom replaces inheritance.
Default method implementations¶
Traits can include default implementations:
trait Greeter {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Hello, {}", self.name())
}
}
greet has a default. Types implementing Greeter only need to provide name; they get greet for free (or can override it).
Functions taking traits¶
Two ways to write "a function that takes anything implementing trait X":
Way 1 - impl Trait:
Way 2 - generic parameter with trait bound:
Both compile to the same thing. The impl Trait form is shorter; the generic form lets you reuse T if mentioned twice.
For taking references to anything implementing a trait:
(Or use trait objects - see "dynamic dispatch" below.)
Generic functions¶
A function that works on any type:
fn largest<T: PartialOrd>(items: &[T]) -> &T {
let mut largest = &items[0];
for item in items {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let nums = vec![10, 25, 3, 99, 42];
println!("{}", largest(&nums)); // 99
let words = vec!["pear", "apple", "banana"];
println!("{}", largest(&words)); // pear (alphabetical)
}
<T: PartialOrd> says: "T is any type that implements PartialOrd" (the trait for <, >, etc.). Inside the function, > works because we required it.
Without the bound, the compiler wouldn't let you use > on T - it doesn't know yet whether T can be compared.
Generic structs¶
You met Vec<T> and HashMap<K, V>. You can write your own:
struct Pair<A, B> {
first: A,
second: B,
}
impl<A, B> Pair<A, B> {
fn new(a: A, b: B) -> Pair<A, B> {
Pair { first: a, second: b }
}
}
fn main() {
let p: Pair<&str, i32> = Pair::new("alice", 30);
println!("{}, {}", p.first, p.second);
}
The <A, B> after impl introduces the type parameters again - needed because the impl block must know what A and B are.
Common standard traits¶
You'll see these often:
| Trait | What it means |
|---|---|
Debug |
type can be printed with {:?} (derivable via #[derive(Debug)]) |
Display |
type can be printed with {} (implement manually) |
Clone |
type can be .clone()d |
Copy |
type can be copied (primitives, etc.) |
PartialEq / Eq |
type supports == |
PartialOrd / Ord |
type supports <, > |
Hash |
type can be a HashMap key |
Default |
type has a default value (T::default()) |
Iterator |
type can be iterated |
The #[derive(...)] attribute auto-implements common traits for your types:
That gives you {:?} printing, .clone(), and == for free. Most structs derive Debug at minimum.
Trait objects: dyn Trait¶
When you want a heterogeneous collection (e.g., a Vec of different types that all implement the same trait), use a trait object:
trait Animal {
fn speak(&self) -> String;
}
// ... Cat, Dog impls ...
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Cat { name: String::from("Whiskers") }),
Box::new(Dog { name: String::from("Rex") }),
];
for a in &animals {
println!("{}", a.speak());
}
}
Box<dyn Animal>- "a heap-allocated something-that-implements-Animal." Thedynkeyword signals dynamic dispatch (the actual method gets resolved at runtime via a vtable, like Java/C++ virtual methods).Box::new(...)puts the value on the heap.
The generic impl Trait form (above) uses static dispatch - the compiler generates a specialized version per concrete type. Faster but produces more code. dyn Trait uses dynamic dispatch - one version, with a small per-call cost.
Rule of thumb: prefer impl Trait when you can; use dyn Trait when you need a heterogeneous collection or to return a value whose type varies.
Lifetimes meet generics (preview)¶
Generic functions returning references need lifetime annotations:
'a is a lifetime parameter. We met this in page 06. Most of the time the compiler infers them; occasionally you write them. Recognize the shape; learn deeply when you encounter a need.
Exercise¶
Replace src/main.rs:
-
Define a trait
Shapewith one required method:fn area(&self) -> f64. -
Define structs
Circle { radius: f64 },Square { side: f64 },Rectangle { width: f64, height: f64 }.#[derive(Debug)]on each. -
Implement
Shapefor each. -
Write a function
fn total_area(shapes: &[Box<dyn Shape>]) -> f64that sums all areas. -
In
main, create a heterogeneousVec<Box<dyn Shape>>with one of each, print each shape with{:?}, and print the total area.
Expected output (numbers may have more decimals):
Circle { radius: 2.0 } area=12.566370614359172
Square { side: 3.0 } area=9.0
Rectangle { width: 4.0, height: 5.0 } area=20.0
total: 41.57
Stretch: define a generic function fn max_by_area<T: Shape>(shapes: &[T]) -> &T that returns the shape with the largest area. Test with a Vec<Circle>.
What you might wonder¶
"Why traits instead of inheritance?" Inheritance forces a hierarchy and tight coupling. A Dog must commit to being-an-Animal at definition time. Traits let any type implement any set of traits in any order. More flexible; fewer accidental dependencies.
"What about default fields, multiple inheritance, etc.?" Rust doesn't have those. Composition (one struct having another as a field) plus traits is the answer.
"Why is Vec<Box<dyn Animal>> so wordy?"
Each part has a purpose: Vec<...> is the list; Box<...> is "owned heap pointer" (needed because trait objects don't have known size); dyn Animal is the trait object. You'll get used to it; it's the cost of explicit memory and dispatch decisions.
"What's the cost of #[derive(Clone)] vs writing it?"
None. Derive generates the same code you'd write. Use it.
Done¶
- Define traits and implement them for types.
- Use trait bounds on generic parameters.
- Use
impl Traitfor function parameters. - Use
Box<dyn Trait>for heterogeneous collections. - Derive
Debug,Clone,PartialEq, etc. with#[derive(...)].
You've now covered the major language features. Next pages: testing, packaging, reading code, contributing.