» Quick Introduction to Rust » 2. Advanced » 2.4 Traits

Traits

A trait is a collection of methods defined for an unknown type: Self.

// Define a trait named `Greet`
trait Greet {
    // A method signature named `greet`
    fn greet(&self);
}

// Implement the `Greet` trait for the `Person` struct
struct Person {
    name: String,
}

impl Greet for Person {
    // Implement the `greet` method for the `Person` struct
    fn greet(&self) {
        println!("Hello, my name is {}!", self.name);
    }
}

fn main() {
    // Create an instance of the `Person` struct
    let person = Person {
        name: String::from("Alice"),
    };

    // Call the `greet` method on the `person` instance
    person.greet();
}

Derive

The compiler is capable of providing basic implementations for some traits via the #[derive] attribute.

The following is a list of derivable traits:

  • Comparison traits: Eq, PartialEq, Ord, PartialOrd.
  • Clone, to create T from &T via a copy.
  • Copy, to give a type 'copy semantics' instead of 'move semantics'.
  • Hash, to compute a hash from &T.
  • Default, to create an empty instance of a data type.
  • Debug, to format a value using the {:?} formatter.
#[derive(PartialEq, PartialOrd)]
struct Duration(f64);

#[derive(Debug)]
struct S(i32);

Return Traits

The Rust compiler needs to know how much space every function's return type requires. This means all your functions have to return a concrete type.

Rust tries to be as explicit as possible whenever it allocates memory on the heap. So if your function returns a pointer that points to a trait on heap, you need to write the return type with the dyn keyword, e.g. Box<dyn Drawable>.

trait Drawable {
    fn draw(&self);
}

struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing a circle with radius {}.", self.radius);
    }
}

struct Square {
    side_length: f64,
}

impl Drawable for Square {
    fn draw(&self) {
        println!("Drawing a square with side length {}.", self.side_length);
    }
}

fn get_shape(is_circle: bool) -> Box<dyn Drawable> {
    if is_circle {
        Box::new(Circle { radius: 5.0 })
    } else {
        Box::new(Square { side_length: 8.0 })
    }
}

fn main() {
    let shape1 = get_shape(true);
    let shape2 = get_shape(false);

    shape1.draw();
    shape2.draw();
}

Operator Overloading

In Rust, many of the operators can be overloaded via traits.

use std::ops::Add;

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let point1 = Point { x: 1, y: 2 };
    let point2 = Point { x: 3, y: 4 };
    let result = point1 + point2;
    println!("Result: {:?}", result);
    // Result: Point { x: 4, y: 6 }
}

Drop

The Drop trait only has one method: drop, which is called automatically when an object goes out of scope. The main use of the Drop trait is to free the resources that the implementor instance owns.

struct CustomResource {
    data: String,
}

impl Drop for CustomResource {
    fn drop(&mut self) {
        println!("Dropping CustomResource with data: {}", self.data);
    }
}

fn main() {
    let resource = CustomResource {
        data: String::from("learning on literank.com"),
    };

    println!("Using the CustomResource");

    // At the end of this block, `resource` will go out of scope,
    // and the `drop` method will be called automatically.
}

Iterators

The Iterator trait is used to implement iterators over collections such as arrays.

struct Counter {
    current: usize,
    end: usize,
}

impl Iterator for Counter {
    type Item = usize;

    // Define the `next` method, which returns the next value in the sequence
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.end {
            let result = Some(self.current);
            self.current += 1;
            result
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter { current: 0, end: 5 };

    // Use the `for` loop with the iterator to iterate over the values
    for num in counter {
        println!("Count: {}", num);
    }

    // You can also use other methods provided by the `Iterator` trait
    let squares: Vec<usize> = Counter { current: 0, end: 5 }
        .map(|x| x * x)
        .collect();

    println!("Squares: {:?}", squares);
}

impl Trait

impl Trait can be used in two situations:

  • as an argument type
  • as a return type

As Argument type

trait Printer {
    fn print(&self);
}

fn print_shape(shape: impl Printer) {
    shape.print();
}

As Return Type

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

fn create_shape() -> impl Shape {
  Circle { radius: 3.0 }
}

fn main() {
    let shape = create_shape();
    println!("Area: {}", shape.area());
}

Clone

When dealing with resources, the default behavior is to transfer them during assignments or function calls. However, sometimes we need to make a copy of the resource as well.

The Clone trait helps us do exactly this.

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

fn main() {
    let person1 = Person {
        name: String::from("Alice"),
        age: 20,
    };

    // Clone the `Person` instance to create a new one
    let person2 = person1.clone();

    // Print the original and cloned persons
    println!("Original Person: {:?}", person1);
    // Original Person: Person { name: "Alice", age: 20 }

    println!("Cloned Person: {:?}", person2);
    // Cloned Person: Person { name: "Alice", age: 20 }
}

Super Trait

Rust doesn't have "inheritance", but you can define a trait as being a "super trait" of another trait.

trait Printable {
    fn print(&self);
}

// Define a sub trait named `DebugPrintable` that extends `Printable`
trait DebugPrintable: Printable {
    fn debug_print(&self);
}

// Implement the `Printable` trait for the `Circle` struct
struct Circle {
    radius: f64,
}

impl Printable for Circle {
    fn print(&self) {
        println!("Printing a circle with radius {}", self.radius);
    }
}

// Implement the `DebugPrintable` trait for the `Circle` struct
impl DebugPrintable for Circle {
    fn debug_print(&self) {
        println!("Debug printing a circle with radius {}", self.radius);
    }
}

fn main() {
    // Create an instance of `Circle`
    let circle = Circle { radius: 3.0 };

    // Call methods from both traits
    circle.print(); // Printing a circle with radius 3
    circle.debug_print(); // Debug printing a circle with radius 3
}

Code Challenge

Try to modify the code provided in the editor to play with traits.

Loading...
> code result goes here
Prev
Next