Traits are a mechanism to define shared behavior between types. It is similar to abstract classes in other languages.

Basic definition

trait Greet {
    fn greet(&self); // Method that must be implemented
}

Implementation

impl Greet for Person {
    fn greet(&self) {
        println!("Hello, I'm {}", self.name);
    }
}
 
person.greet();

orphan rule

A trait can be implemented as long as:

  1. The type is yours.
  2. The trait is yours.
  3. Both the type and the trait are yours.

This allows adding behavior to types from external libraries:

// custom trait for an external type
impl Repeat for String {
   fn repeat(&self) {
       // implementation...
   }
}
s.repeat();
 
// external trait for a custom type
struct Person;
impl std::fmt::Display for Person {
   // implementation...
}
 
// external trait for an external type
impl std::fmt::Display for String {
   // ERROR
}

Default values

A trait can provide default implementations that types can use or override:

trait Greet {
    fn greet(&self) {
        println!("Hello, I'm {}", self.name);
    }
}
 
impl Greet for Person {} // Uses the default implementation

Warning

The default implementation has access to self.name, but the compiler cannot verify that all types implementing the trait have a name field. If a type does not have it, you will get an error when implementing.

Trait as parameter

Allows creating functions that accept any type that implements a trait:

fn show(s: &impl Greet) {
    s.greet();
}

Alternative syntax (trait bound)

fn show<T: Greet>(s: &T) {
    s.greet();
}
 
// Multiple constraints
fn show_detailed<T: Greet + Debug>(s: &T) {
    println!("{:?}", s);
    s.greet();
}

Trait as return type

Allows a function to return any type that implements a trait:

fn create(name: String, age: u32) -> impl Greet {
    Person { name, age }
}

Warning

When impl Trait is used as a return type, the function can only return a single concrete type. You cannot return Person in one case and Animal in another.

fn create(b: bool) -> impl Greet {  // ERROR
   if b {
       Person
   } else {
       Dog
   }
}

To return any type that implements a trait you must use Box<dyn>. See Smart Pointers.

fn create(b: bool) -> Box<dyn Greet> {
    if b {
        Box::new(Person)
    } else {
        Box::new(Dog)
    }
}

Derived traits

They can be implemented automatically with #[derive(...)]. See procedural macros:

#[derive(Debug)]
struct Alien {
    name: String,
    planet: String,
}
TraitDescriptionUsage example
DebugPrinting with {:?}println!("{:?}", person)
CloneCopying with .clone()person.clone()
CopyImplicit copiesAssignment without move
PartialEq== and != comparisona == b
EqTotal equalityExtends PartialEq
PartialOrd<, >, <=, >= comparisona < b
OrdTotal orderFor sort()
HashHash computationFor HashMap, HashSet
DefaultDefault valueType::default()
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
 
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1.clone();       // Clone
println!("{:?}", p1);      // Debug
let equal = p1 == p2;      // PartialEq

Super traits

A trait can require that types implementing it also implement another trait:

trait SayName: Greet {
    fn name(&self);
}
 
// To implement SayName, the type MUST ALSO implement Greet

Tip

Super traits are useful when your trait depends on the behavior of another. For example, Display: Debug guarantees that every type that is displayable is also debuggable.

Traits and standard types

Many standard library types implement common traits:

// Option and Result implement many traits
let opt: Option<i32> = Some(42);
println!("{:?}", opt);     // Debug
let opt2 = opt.clone();    // Clone

Note

Closures implement the Fn, FnMut and FnOnce traits depending on how they capture variables. This allows passing closures as arguments of generic functions.

Next: Option and Result