Structs, Traits and Implementations
Rust has structs and traits, and no inheritance. A struct holds data; an impl
block gives it behaviour; a trait describes behaviour several types can share.
struct Temperature {
celsius: f64,
}
impl Temperature {
// An associated function: no self, called as Temperature::new.
fn new(celsius: f64) -> Result<Self, String> {
if celsius < -273.15 {
return Err(format!("below absolute zero: {celsius}"));
}
Ok(Self { celsius })
}
// A method: &self borrows, so the caller keeps the value.
fn fahrenheit(&self) -> f64 {
self.celsius * 9.0 / 5.0 + 32.0
}
// &mut self to change it; self alone consumes it.
fn warm(&mut self, by: f64) {
self.celsius += by;
}
fn into_celsius(self) -> f64 {
self.celsius
}
}
fn main() {
let mut t = Temperature::new(21.0).expect("valid");
println!("{:.1}C = {:.1}F", t.celsius, t.fahrenheit());
t.warm(5.0);
println!("{:.1}C", t.celsius);
match Temperature::new(-300.0) {
Ok(_) => println!("unexpected"),
Err(e) => println!("rejected: {e}"),
}
println!("{:.1}", t.into_celsius());
// println!("{}", t.celsius); // moved by into_celsius
}
There is no constructor keyword. new is a convention, and returning Result
from it is how you make an invalid value impossible to build.
Traits: shared behaviour without a base class
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> String {
// A default implementation, which an implementor may override.
String::from("shape")
}
fn describe(&self) -> String {
format!("{} of area {:.2}", self.name(), self.area())
}
}
struct Rectangle {
width: f64,
height: f64,
}
struct Circle {
radius: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn name(&self) -> String {
String::from("rectangle")
}
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
// name() is left as the default.
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Rectangle { width: 3.0, height: 4.0 }),
Box::new(Circle { radius: 1.0 }),
];
for shape in &shapes {
println!("{}", shape.describe());
}
// Static dispatch: the compiler generates a copy per concrete type.
println!("{}", describe_static(&Rectangle { width: 1.0, height: 2.0 }));
}
// impl Trait: static dispatch, no allocation, resolved at compile time.
fn describe_static(shape: &impl Shape) -> String {
shape.describe()
}
&impl Shape is static dispatch - one specialised copy per type, no runtime
cost. Box<dyn Shape> is dynamic dispatch - one copy of the code, a vtable
lookup per call, and the ability to hold different types in one collection.
Reach for the first unless you need the second.
derive: the traits you get for free
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
struct Point {
x: i32,
y: i32,
}
use std::collections::HashSet;
fn main() {
let a = Point { x: 1, y: 2 };
let b = a.clone();
println!("{a:?}"); // Debug
println!("{a:#?}", ); // pretty Debug
println!("{}", a == b); // PartialEq
println!("{:?}", Point::default()); // Default: all fields zeroed
let set: HashSet<Point> = vec![a.clone(), b].into_iter().collect();
println!("{} unique", set.len()); // Hash + Eq
let mut points = vec![Point { x: 2, y: 0 }, Point { x: 1, y: 9 }];
points.sort(); // Ord, field by field
println!("{points:?}");
}
Deriving PartialEq and Hash together keeps the contract that a HashMap
depends on - two equal values hash the same - without you writing either.
Implementing standard traits yourself
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Money {
pence: i64,
}
// Display is what {} uses; Debug ({:?}) is for developers.
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.2}", self.pence as f64 / 100.0)
}
}
// Operator overloading is a trait like any other.
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { pence: self.pence + other.pence }
}
}
// From gives you Into for free, and is the idiomatic conversion.
impl From<i64> for Money {
fn from(pence: i64) -> Self {
Money { pence }
}
}
fn main() {
let a = Money::from(1999);
let b: Money = 250.into();
println!("{a} + {b} = {}", a + b);
println!("{:?}", a + b);
println!("{}", a == Money { pence: 1999 });
}
Generics with trait bounds
use std::fmt::Display;
// The bound is what lets the body call anything at all.
fn largest<T: PartialOrd + Copy>(values: &[T]) -> T {
let mut best = values[0];
for &value in values {
if value > best {
best = value;
}
}
best
}
// A where clause, when the bounds get long.
fn describe_all<T>(values: &[T]) -> String
where
T: Display,
{
values.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")
}
// Generic structs, with the impl block carrying the same parameter.
struct Pair<T> {
left: T,
right: T,
}
impl<T: Display + PartialOrd> Pair<T> {
fn larger(&self) -> &T {
if self.left > self.right { &self.left } else { &self.right }
}
}
fn main() {
println!("{}", largest(&[3, 9, 4]));
println!("{}", largest(&[1.5, 0.2]));
println!("{}", describe_all(&["pear", "apple"]));
println!("{}", Pair { left: 10, right: 3 }.larger());
}
What to take away
implblocks hold behaviour;newis a convention, not a keyword.&selfborrows,&mut selfborrows mutably,selfconsumes.- Traits give shared behaviour with no inheritance, and may carry defaults.
impl Traitis static dispatch;Box<dyn Trait>is dynamic, for mixed collections.#[derive(...)]writes Debug, Clone, PartialEq, Hash, Default and Ord for you.- Implement
Display,AddandFromto make your type behave like a built-in one.