Traits in Rust
Rust's mechanism for shared behavior across types — similar to interfaces, with defaults and trait bounds most languages lack.
3 min read
Rust doesn't have inheritance. Instead, shared behavior across unrelated types is expressed through traits — a set of method signatures a type can promise to implement, similar to an interface in Java or TypeScript, but woven more deeply into the language.
Defining and implementing a trait
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
body: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}...", self.title, &self.body[..20.min(self.body.len())])
}
}
fn main() {
let article = Article {
title: String::from("Rust 1.0"),
body: String::from("Rust reached its first stable release in 2015."),
};
println!("{}", article.summarize());
}Any type can implement Summary, as long as it provides summarize. Code that only needs to call .summarize() doesn't need to know or care whether it's holding an Article, a Tweet, or anything else that implements the trait.
Default implementations
A trait method can supply a default body, which implementers can use as-is or override:
trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
impl Summary for Article {} // uses the default, no override neededTraits as parameters
This is where traits start doing real work — accepting "anything that implements this trait" as a parameter, instead of one concrete type:
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}notify can be called with an &Article, a &Tweet, or any other type implementing Summary — the function is written once, against the behavior it needs, not against a specific type.
Trait bounds, the fuller syntax
&impl Summary is shorthand for a more general form called a trait bound, which matters once you need the same type to appear more than once, or need multiple constraints:
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
fn compare_and_notify<T: Summary + PartialEq>(a: &T, b: &T) {
if a == b {
println!("Identical: {}", a.summarize());
}
}T: Summary + PartialEq reads as "some type T, which must implement both Summary and PartialEq." This is central to how generics (the next lesson) stay useful without giving up compile-time type checking.
Common derivable traits
Several traits are common enough to auto-generate with #[derive(...)], rather than writing the impl block by hand:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}Debug enables {:?} printing (seen in the structs lesson), Clone enables .clone(), and PartialEq enables == comparisons. The compiler generates a reasonable implementation of each automatically, which is why you'll see #[derive(...)] on almost every struct and enum in real Rust code.
Traits vs inheritance
Where a class hierarchy forces you to decide up front how types relate to each other, traits let unrelated types share behavior without being related at all — a Duck and an Airplane could both implement Flies without sharing any other structure. This composition-over-inheritance approach is one of the more significant departures from traditional object-oriented languages, and it's worth sitting with, since most of Rust's standard library (Iterator, Display, Clone, and more) is built entirely out of traits like the ones you just wrote.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.