Rust - Traits


Traits in Rust are similar to interfaces in other languages. When a type implements a trait, that type will behave as the trait describes.

pub trait Shape {
    fn area(&self) -> f32;  // Traits declarations have ';' as opposed to {} blocks
}

pub struct Circle {
    radius: f32,
}

impl Shape for Circle {
    fn area(&self) -> f32 {
        self.radius * 3.14159
    }
}

pub struct Rectable {
    width: f32,
    height: f32,
}

impl Shape for Rectable {
    fn area(&self) -> f32 {
        self.width * self.height
    }
}

fn main() {
    let cir = Circle { radius: 12.25 };
    println!("{}", cir.area());
}

Associated Types

Associated types act as a type placeholder for traits in method signatures. When the trait is implemented, the associated type is replaced with the concrete type.

struct Point {x: i32, y: i32}

trait Contains {
    // Declare placeholder types
    type A;
    type B;

    fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
}

impl Contains for Point {
    // Specify types during implementation
    type A = i32;
    type B = i32;

    fn contains(&self, num1: &Self::A, num2: &Self::B) -> bool {
        (&self.x == num1) && (&self.y == num2)
    }
    // This would also be a valid signature:
    //fn contains(&self, num1: &i32, num2: &i32) -> bool {}
}

fn main() {
    let num1 = 2;
    let num2 = 3;

    let points = vec![Point{x: 1, y: 2}, Point{x: 2, y: 3}];

    for point in points {
        match point.contains(&num1, &num2) {
            true => println!("Does contain point!"),
            false => println!("Does NOT contain point!"),
        }
    }
}

Conditional Method Implementation

You can use traits to conditionally implement methods to a generic struct. Example:

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self {x, y}  // Notice Self vs self
    }
}

impl<T: Display> Pair<T> {
    fn print(&self) {
        println!("{}, {}", self.x, self.y);
    }
}

fn main() {
    let p = Pair::new(5, 4);
    p.print();
}
Self

In the above example, you might notice the capital 'S' in Self. Capital Self can be used to refer to the type that is being implemented as opposed to self which refers to the calling object.

Default Generic Type

A default generic type can be given for traits.

There are two main purposes for this:

  • Extend a type without breaking existing code
  • Customize specific cases
use std::ops::Add;

struct Feet(f32);
struct Inches(f32);

/* // Add trait definition
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
*/

// Use default
impl Add for Feet {
    type Output = Feet;

    fn add(self, other: Feet) -> Feet {
        Feet(self.0 + other.0)
    }
}

impl Add<Inches> for Feet {
    type Output = Feet;

    fn add(self, other: Inches) -> Feet {
        Feet(self.0 + (other.0 / 12.0))
    }
}

Default Implementation

To have a default implementation for a trait, use a {} block.

Default implementations can also call other functions.

Example:

pub trait Shape {
    fn area(&self) -> f32 {
        0.0
    }
}

Drop

The drop trait is similar to the deconstructor in C++. When a variable that has resources on the heap, it needs to implement the drop trait so when the variable leaves its scope, the resources are deallocated. If the drop trait in implemented, the copy trait cannot be.

Supertraits

Supertraits allow a trait to require the implementation of another trait. Example:

use std::ops::Add;

#[derive(Debug, PartialEq, Copy, Clone)]
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 }
    }
}

// Supertrait - Has ':' with trait requirements
trait Print: std::fmt::Debug {
    fn print(&self) {
        println!("*** {:?} *** ", self);
    }
}

impl Print for Point {}

fn main() {
    let p1 = Point{x: 1, y: 2};
    p1.print();
}

Traits As Parameters

Traits can also be parameters. These functions are similar to generics, but restrict the types to types that implement the trait. Example:

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

pub trait TwoD {}

pub struct Circle {
    radius: f32,
}

impl Shape for Circle {
    fn area(&self) -> f32 {
        self.radius * 3.14159
    }
}

impl TwoD for Circle {}

// Traits as parameters - short hand
pub fn print_area(s: &impl Shape) {
    println!("{}", s.area());
}

// Traits as parameters - the long way
pub fn print_area2<T: Shape>(s: &T) {
    println!("{}", s.area());
}

// Multiple Traits
pub fn print_area3<T: Shape + TwoD>(s: &T) {
    println!("{}", s.area());
}

// where can be used to make trait bounds look pretty
pub fn print_areas<T, U>(s: &T, r: &U) 
    where T: Shape + TwoD,
          U: Shape
{
    println!("{}", s.area());
    println!("{}", r.area());
}

// Return type that implements trait
pub fn get_area(s: &str) -> impl Shape {
    if s == "Circle" {
        return Circle { radius: 0.0 };
    }
    Circle { radius: 0.0 }  // Imagine other Shapes being here
}

fn main() {
    let cir = Circle { radius: 12.25 };
    print_area3(&cir);
    print_areas(&cir, &cir);
}