Chapter 05
No inheritance, no base classes. Shared behavior comes from traits, and they're more flexible than C# interfaces in one big way: you can implement a trait for a type you didn't define.
A trait declares a set of methods; an impl ... for block provides them for a type. Structurally it's an interface.
interface IArea {
double Area();
}
class Circle : IArea {
public double Area() => ...;
}
trait Area {
fn area(&self) -> f64;
}
impl Area for Circle {
fn area(&self) -> f64 { ... }
}
You can impl your own trait for i32, String, or any third-party type — something C# only approximates with extension methods, and those can't satisfy an interface. This "implement X for a type I don't own" is why Rust traits feel so composable. (The one rule: you must own either the trait or the type — the "orphan rule" — which prevents conflicting impls.)
Generic constraints in C# (where T : IArea) become trait bounds. Same idea, tighter syntax.
// "for any T that implements Area"
fn describe<T: Area>(shape: &T) {
println!("area = {}", shape.area());
}
// equivalent, often clearer:
fn describe(shape: &impl Area) { ... }
Both generics/impl Trait and dyn Trait let you write code against "any type that has an area() method." The difference is when the program figures out which area() to actually call — and that timing has a performance cost. C# makes this choice for you and mostly hides it; Rust puts it in the type so you can see it. Here are the two strategies, one at a time.
When you call a generic function, the compiler already knows the exact concrete type at each call site. So it does monomorphization: it stamps out a separate, specialized copy of the function for every type you actually use it with, and wires the correct area() call directly into each copy. Nothing is looked up while the program runs — the call is as direct as if you'd hand-written one version for Circle and another for Rect.
// you write this generic function ONCE...
fn describe<T: Area>(shape: &T) {
println!("area = {}", shape.area());
}
describe(&circle); // compiler generates a copy specialized for Circle
describe(&rect); // ...and a separate copy specialized for Rect
This is what C++ templates do, and what C# does when a generic method runs on a value type (which is why there's no boxing). You pay for it in binary size — one machine-code copy per type — but each call itself is as fast as it can be.
Sometimes you genuinely can't know the concrete type until the program runs. The classic case is wanting one collection that holds a mix of types — a list of assorted shapes. Monomorphization can't help here, because a single Vec needs one element type. The answer is dyn Trait, used behind a pointer such as Box<dyn Area>.
A dyn value carries a hidden pointer to a vtable — a little table of function pointers for that trait. When you call s.area(), the program looks area up in that table and calls whatever it points to. That's one extra pointer-hop per call, but there's only ever a single copy of the code.
// one list, two different concrete types living inside it
let shapes: Vec<Box<dyn Area>> = vec![
Box::new(circle),
Box::new(rect),
];
for s in &shapes {
// the right area() is found via the vtable, per element
println!("area = {}", s.area());
}
This is exactly what happens when you call a method through a C# interface reference (IArea) — a vtable lookup. The keyword dyn is just Rust stating plainly that the call is resolved at runtime.
In C# you rarely think about this: the runtime specializes generics over value types for you, and interface calls quietly go through a vtable. Rust exposes the same two mechanisms in the type — <T: Trait>/impl Trait for static, dyn Trait for dynamic — so the cost is never hidden from you.
The two side by side:
impl Trait / <T: Trait> | dyn Trait | |
|---|---|---|
| Type known | at compile time | at runtime |
| How the call resolves | wired in directly (static) | vtable lookup (dynamic) |
| Mechanism | monomorphization — a specialized copy per type | one copy; the value carries a pointer + vtable |
| Cost | no runtime overhead, larger binary | tiny per-call indirection, smaller binary |
| C# analogue | generic method over a value type (no boxing) | calling through an interface reference |
Default to generics / impl Trait — it's the common case and costs nothing at runtime. Reach for Box<dyn Trait> when you need to keep different concrete types together behind one trait (the job List<IArea> does in C#), or when all those specialized copies would bloat the binary more than you'd like.
Many traits are mechanical, so the compiler can write them for you with #[derive(...)] — comparable to source generators or records auto-implementing equality.
// auto-implements Debug, Clone, equality, hashing
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64); // tuple struct ≈ a strong-typed wrapper
Debug ≈ a good ToString() for diagnostics (printed with {:?}). Clone ≈ an explicit deep-ish copy. PartialEq/Eq ≈ Equals/==. Deriving them is one line versus the hand-written boilerplate you'd write (or have a record generate) in C#.