🎓 A1Learner
🔍

Rust Traits Basics

A trait is a list of behaviours that a type can promise to have. A type joins the list by writing each behaviour itself.

The same job under two names

A cat and a dog both describe themselves. With plain functions, each type gets its own function name. Nothing says the two jobs are the same job:

struct Cat {
    name: String,
}

struct Dog {
    name: String,
}

fn describe_cat(cat: &Cat) -> String {
    format!("{} the cat", cat.name)
}

fn describe_dog(dog: &Dog) -> String {
    format!("{} the dog", dog.name)
}

fn main() {
    let cat = Cat { name: String::from("Milo") };
    let dog = Dog { name: String::from("Rex") };
    println!("{}", describe_cat(&cat));
    println!("{}", describe_dog(&dog));
}

A trait names the job once

The trait lists the behaviour and nothing else. An impl block then writes the behaviour for one type:

trait Describe {
    fn describe(&self) -> String;
}

struct Cat {
    name: String,
}

impl Describe for Cat {
    fn describe(&self) -> String {
        format!("{} the cat", self.name)
    }
}

fn main() {
    let cat = Cat { name: String::from("Milo") };
    println!("{}", cat.describe());
}

Another type joins the same list

A Dog writes the same behaviour in its own words. Both values now answer to the same method name:

trait Describe {
    fn describe(&self) -> String;
}

struct Cat {
    name: String,
}

struct Dog {
    name: String,
}

impl Describe for Cat {
    fn describe(&self) -> String {
        format!("{} the cat", self.name)
    }
}

impl Describe for Dog {
    fn describe(&self) -> String {
        format!("{} the dog", self.name)
    }
}

fn main() {
    let cat = Cat { name: String::from("Milo") };
    let dog = Dog { name: String::from("Rex") };
    println!("{}", cat.describe());
    println!("{}", dog.describe());
}

derive writes a trait for you

Debug and Clone are traits too. The derive attribute asks the compiler to write them, so the struct can print and copy itself:

#[derive(Debug, Clone)]
struct Cat {
    name: String,
}

fn main() {
    let cat = Cat { name: String::from("Milo") };
    let copy = cat.clone();
    println!("{:?}", cat);
    println!("{:?}", copy);
}

Example

trait Greet {
    fn greet(&self) -> String;
}

#[derive(Debug, Clone)]
struct Player {
    name: String,
    level: u32,
}

impl Greet for Player {
    fn greet(&self) -> String {
        format!("Hello, I am {} at level {}", self.name, self.level)
    }
}

struct Robot {
    id: u32,
}

impl Greet for Robot {
    fn greet(&self) -> String {
        format!("Beep. Robot {} ready.", self.id)
    }
}

fn main() {
    let player = Player { name: String::from("Ada"), level: 3 };
    let twin = player.clone();
    println!("{}", player.greet());
    println!("{}", Robot { id: 7 }.greet());
    println!("twin = {:?}", twin);
}

Key points

  • One trait names the job once, and any type can sign up to do it.
  • Use impl TraitName for TypeName to write a type's behaviours.
  • A trait method is called with a dot, like any other method.
  • #[derive(...)] asks the compiler to write an everyday trait, like Debug or Clone, for the type.

In this example, Greet is a trait with one behaviour, greet. Player writes it with its name and level. Robot writes it with its id. The two types share no fields, yet both answer to greet(). The Player also derives Debug and Clone. The clone makes twin, and {:?} prints it. Rust Traits, in the Intermediate course, goes deeper from here — who may write an impl, and traits that rest on other traits.

Note

Rust traits resemble interfaces in other languages.

The impl block can live apart from the type it serves.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What does impl Describe for Cat say?