Rust Traits
Defining Traits
Traits define shared behavior
trait Describe {
fn describe(&self) -> String;
// Default implementation — can be overridden
fn summary(&self) -> String {
format!("({})", self.describe())
}
}
Traits are Rust’s interfaces. They define a set of methods a type must implement. Default implementations provide fallback behavior.
Implementing Traits
impl Trait for Type
struct Host {
name: String,
ip: String,
}
impl Describe for Host {
fn describe(&self) -> String {
format!("{} ({})", self.name, self.ip)
}
// summary() uses the default implementation
}
let h = Host { name: "sw01".into(), ip: "10.50.1.10".into() };
println!("{}", h.describe()); // "sw01 (10.50.1.10)"
println!("{}", h.summary()); // "(sw01 (10.50.1.10))"
Trait Bounds
Constrain generic types
// Function that accepts any type implementing Describe
fn print_item(item: &impl Describe) {
println!("{}", item.describe());
}
// Equivalent with explicit syntax
fn print_item<T: Describe>(item: &T) {
println!("{}", item.describe());
}
// Multiple bounds
fn process<T: Describe + Clone + Debug>(item: &T) {
println!("{:?}", item);
}
// where clause — cleaner for complex bounds
fn complex<T, U>(t: &T, u: &U) -> String
where
T: Describe + Clone,
U: Describe + Debug,
{
format!("{} + {:?}", t.describe(), u)
}
Common Standard Library Traits
The traits you will implement most often
use std::fmt;
// Display — user-facing string representation
impl fmt::Display for Host {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.name, self.ip)
}
}
println!("{}", host); // uses Display
// From/Into — type conversion
impl From<&str> for Host {
fn from(s: &str) -> Self {
let parts: Vec<&str> = s.splitn(2, '@').collect();
Host {
name: parts[0].into(),
ip: parts.get(1).unwrap_or(&"0.0.0.0").to_string(),
}
}
}
let h: Host = "sw01@10.50.1.10".into();
// Iterator
impl Iterator for PortRange {
type Item = u16;
fn next(&mut self) -> Option<Self::Item> {
if self.current <= self.end {
let port = self.current;
self.current += 1;
Some(port)
} else {
None
}
}
}
Display enables {} formatting. From/Into enable type conversions. Implementing From<A> for B gives you Into<B> for A automatically.
Trait Objects (Dynamic Dispatch)
dyn Trait — runtime polymorphism
fn log_items(items: &[&dyn Describe]) {
for item in items {
println!("{}", item.describe());
}
}
// Box<dyn Trait> — owned trait object
fn create_device(kind: &str) -> Box<dyn Describe> {
match kind {
"host" => Box::new(Host { name: "sw01".into(), ip: "10.50.1.10".into() }),
"vm" => Box::new(VM { name: "web01".into() }),
_ => panic!("unknown device"),
}
}
dyn Trait uses dynamic dispatch (vtable lookup at runtime). impl Trait uses static dispatch (monomorphization at compile time). Static is faster but produces more binary code.
Supertraits
Requiring another trait as a prerequisite
trait Loggable: Display + Debug {
fn log(&self) {
println!("[LOG] {}", self); // uses Display
println!("[DEBUG] {:?}", self); // uses Debug
}
}
A supertrait bound means any type implementing Loggable must also implement Display and Debug.
Orphan Rule
You can implement a trait only if you own the trait or the type
// OK — you own Host
impl Display for Host { ... }
// OK — you own Describe
impl Describe for String { ... }
// ERROR — you own neither Vec nor Display
// impl Display for Vec<i32> { ... }
// Workaround: newtype pattern
struct HostList(Vec<Host>);
impl Display for HostList { ... }
The orphan rule prevents conflicting implementations across crates. The newtype pattern wraps a foreign type to make it yours.