Traits

A trait is a contract that defines a set of behaviors or properties that a type must implement. It can contain associated types, constants, function or method signatures, and overridable default implementations.

Definition

With No Associates

πŸ’‘ Mostly used to mark a type as having certain properties to allow in certain operations. Known as Marker Traits.

pub trait Sized { }

With the Declarations of Associates

trait Greet {
    const PREFIX: &'static str;
    type Item;
    fn greet(&self) -> String;
}

With the Default Implementations of Associates

trait Greet {
    const PREFIX: &'static str = "Hello";
    
    fn greet(&self) -> String {
        format!("{}!", String::from(Self::PREFIX))
    }
}

With Supertraits

A trait must have to be implemented first before implementing the current trait.

pub trait Copy: Clone { } // πŸ’‘ Any type that copyable should be clonable

// πŸ’‘ Any type that clonable should be sized
pub trait Clone: Sized {
    fn clone(&self) -> Self;
    fn clone_from(&mut self, source: &Self) { ... }
}

πŸ’― trait Subtrait: Super or trait Subtrait: SupertraitA + SupertraitB

With Generic Types

// πŸ’‘ Convert from one type to another. If successful, retrun Type; else return associated Error
pub trait TryFrom<T>: Sized {
    type Error;

    fn try_from(value: T) -> Result<Self, Self::Error>;
}

Trait Impls (Manual Implementation)

Manually implementing a shared behavior defined in a trait for a type via an impl block.

With Required Components

When a trait has only declarations of associated items, it’s required to implement all of them.

struct Person {
    name: String
}

trait Greet {
    const PREFIX: &'static str; // πŸ’‘ Required constant
    fn greet(&self) -> String; // πŸ’‘ Required method
}

impl Greet for Person {
    const PREFIX: &'static str = "Hello";
    
    fn greet(&self) -> String {
        format!("{} {}!", Self::PREFIX.to_owned(), self.name)
    }
}

fn main() {
    let steve = Person { name: "Steve".to_string() };
    println!("{}", steve.greet()); // Hello Steve!
}

With Provided Components

When a trait has default values and default implementations, it’s possible to implement only some of them or override them.

struct Person {
    name: String
}

trait Greet {
    // πŸ’‘ Provided constant
    const PREFIX: &'static str = "Hello";

    // πŸ’‘ Provided method
    fn greet(&self) -> String {
        format!("{}!", String::from(Self::PREFIX))
    }
}

impl Greet for Person {
    // πŸ’‘ Overridden constant
    const PREFIX: &'static str = "Good morning";
    
    // πŸ’‘ Overridden method
    fn greet(&self) -> String {
        format!("{} {}!", Self::PREFIX.to_owned(), self.name)
    }
}

fn main() {
    let steve = Person { name: "Steve".to_string() };
    println!("{}", steve.greet()); // Good morning Steve!
}

For Enum Types

#![allow(unused)]
enum Shape {
    Circle(f64),         // radius
    Rectangle(u32, u32), // width, height
}

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

impl Area for Shape {
    fn area(&self) -> f64 {
        match *self {
            Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
            Shape::Rectangle(width, height) => (width * height) as f64,
        }
    }
}

fn main() {
    let circle = Shape::Circle(7.0);
    let rect = Shape::Rectangle(5, 5);

    println!("{:?}", circle.area());
    println!("{:?}", rect.area());
}

For Generic Types

  1. impl<T> Trait for Type<T>
#![allow(unused)]
struct Person<T> {
    name: String,
    age: T,
}

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

impl<T> Greet for Person<T> {
    fn greet(&self) -> String {
        format!("Hello {}!", self.name)
    }
}

fn main() {
    let steve = Person { name: "Steve".to_string(), age: 65 }; // πŸ’‘ age: i32
    let bill = Person { name: "Bill".to_string(), age: 7.5 }; // πŸ’‘ age: f64

    println!("{:?}", steve.greet()); // Hello Steve!
    println!("{:?}", bill.greet()); // Hello Bill!
}
  1. impl<T> Trait<T> for Type<T>
struct Point<T> {
    x: T,
    y: T,
}

trait IntoTuple<T> {
    fn into_tuple(self) -> (T, T);
}

impl<T> IntoTuple<T> for Point<T> {
    fn into_tuple(self) -> (T, T) {
        (self.x, self.y)
    }
}

fn main() {
    let a = Point { x: 0, y: 1 }; // Point<i32>
    let b = Point { x: "2.0", y: "2.2" }; // Point<&str>

    println!("{:?}", a.into_tuple()); // (0, 1)
    println!("{:?}", b.into_tuple()); // ("2.0", "2.2")
}

πŸ‘¨β€πŸ« Update the above implementation to impl<T, U> Trait<T> for Type<U>.

Derive and Auto Traits (Autogenerate Implementations)

Derive Traits

The compiler generates the trait implementation automatically for you, based on the derived attributes.

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

fn main() {
    let steve = Person { name: "Steve".to_string() };
    let bill = Person { name: "Bill".to_string() };

    // Debug: πŸ’‘ allow debug print with {:?}
    println!("{steve:?}"); // Person { name: "Steve" }

    // Clone: πŸ’‘ allow duplicate data via .clone()
    let gates = bill.clone();

    // PartialEq: πŸ’‘ support equate the two instances via == and !=
    println!("{}", steve == bill); // false
    println!("{}", bill == gates); // true
}

πŸ”Ž Check bellow Rust STD documentation pages, of these normal derive-traits.

TraitThe functionality (implement to the type at compile-time)
DebugEnables debug-printing of the internal state with {:?}
DefaultAllows creating a default initial value with ::default()
CloneAllows creating a deep copy explicitly with .clone()
PartialEq / EqAllows comparing instances with == and != operators
PartialOrd / OrdAllows comparing instances with <, <=, >, and >= operators
HashAllows the type to be used as a key in a HashMap or HashSet

πŸ’― Marker traits are the traits that have no associated constants, types, methods, etc. So, the compiler-generated implementation has only an empty impl Trait for Type { } block.

#![allow(unused)]

#[derive(Debug, Clone, Copy)] // πŸ’‘ Copy trait needs the Clone trait as a supertrait
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let a = Point { x: 0, y: 1 };
    let b = a; // Copy: πŸ’‘ duplicate data. a and b are separate instances

    println!("{a:?}");
    println!("{b:?}");
}
// πŸ‘¨β€πŸ« Try remove Copy derive on Point and explisitly copy via .clone()

πŸ”Ž Copy set the ability to duplicate via simply copying bits (pass by duplicating)

Auto Traits

Implicitly bound/ automatically implemented by the compiler without any keywords.

pub trait Sized { }
pub unsafe auto trait Sync { }
pub auto trait Unpin { }
#![allow(unused)]

#[derive(Debug)]
struct Point { // πŸ’‘ Sized, Send, Sync: (Automatic) Implicitly bound for most simple types in Rust
    x: i32,
    y: i32,
}

fn main() {
    let a = Point { x: 0, y: 1 };

    std::thread::scope(|s| {
        s.spawn(|| println!("{a:?}"));
        s.spawn(|| println!("{a:?}"));
    });

    std::thread::spawn(move || println!("{a:?}")).join().unwrap();
}
TraitThe properties (set to the type automatically)
SizedAble to determine the size at compile time
UnpinSafe to move in memory
SendSafe to move ownership across threads
SyncSafe to shared references between threads
UnwindSafeSafe to use in panic unwindings
RefUnwindSafeSafe to use shared references in panic unwinding

πŸ‘¨β€πŸ« Sized/ Unsized

  • ⭐️ Most simple types in Rust have a fixed size known at compile time and automatically implement Sized marker trait.
  • πŸ’― Unsized types (or dynamically sized types, DSTs) cannot be used directly as local variables, function parameters, or return values. They must always be placed behind an indirection, such as a reference (& or &mut), a Box, Rc, or Arc. Common examples include slices ([T], &str) and trait objects (dyn Trait).
    • πŸ”Ž Pointers to these types are known as “wide pointers” or “fat pointers” because they contain both a pointer to the data and additional metadata. Metadata: for slices - length (number of elements) and for trait objects - a pointer to the vtable/ virtual function table. Ex: &[T], *mut str, Box<dyn Trait>
  • πŸ’‘ To allow a type to be unsized (relaxes the default Sized requirement), we can use the ?Sized bound.

Static Dispatch

Monomorphization is a compiler optimization and implementation strategy that transforms polymorphic (generic) code into specialized, monomorphic (single-type) versions for each unique set of concrete types used in a program. This increases the binary size.

Dispatch is the process of deciding which implementation of a polymorphic function to execute, either statically at compile time or dynamically at runtime (via a lookup in a vtable).

Rust compiler generates highly optimized code blocks for each type used for a generic function. So, function calls are statically resolved at compile time and no runtime overhead.

Trait Bounds

Specify the type constraints by traits in generics (<T: Trait>) rather than using exact types. Allowed in a field types & function return and argument.

struct Point { x: i32, y: i32 }
impl Addr for Point {
    fn addr(&self) -> String {
        format!("{}, {}", self.x, self.y)
    }
}

struct MapLocation(String, String); // latitude, longitude
impl Addr for MapLocation {
    fn addr(&self) -> String {
        format!("{}, {}", self.0, self.1)
    }
}

// --- ⭐️ main Trait & Type with Trait Bounds ⭐️
trait Addr {
    fn addr(&self) -> String;
}

struct Delivery<T: Addr> {
    location: T,
}

impl<T: Addr> Delivery<T> {
    fn new(location: T) -> Self {
        Self { location }
    }
}

// --- ⭐️ fn with Trait Bounds ⭐️
fn location_info<T: Addr>(location: T) {
    println!("Latitude/ Longitude: {}", location.addr())
}

// ---
fn main() {
    let ocean = Point { x: 35, y: 20 };
    let tokyo = MapLocation("35.68951".to_string(), "139.69170".to_string());

    let pkg1 = Delivery::new(ocean);
    let pkg2 = Delivery::new(tokyo);

    location_info(pkg1.location); // Latitude/ Longitude: 35, 20
    location_info(pkg2.location); // Latitude/ Longitude: 35.68951, 139.69170
}

Opaque Types

Specify the type constraints via traits (:impl Trait) without specifying full generic syntax. But only allowed as a function return or an argument.

struct Point { x: i32, y: i32 }
impl Addr for Point {
    fn addr(&self) -> String {
        format!("{}, {}", self.x, self.y)
    }
}

struct MapLocation(String, String); // latitude, longitude
impl Addr for MapLocation {
    fn addr(&self) -> String {
        format!("{}, {}", self.0, self.1)
    }
}

trait Addr {
    fn addr(&self) -> String;
}

// --- ⭐️ Opaque Types are only allowed in function parameters 
fn location_info(location: impl Addr) { // πŸ’‘ Argument-Position-Impl-Trait/ APIT
    println!("Latitude/ Longitude: {}", location.addr())
}

fn tokyo_location() -> impl Addr { // πŸ’‘ Return-Position-Impl-Trait/ RPIT
    MapLocation("35.68951".to_string(), "139.69170".to_string())
}

// ---
fn main() {
    let ocean = Point { x: 35, y: 20 };
    let tokyo = tokyo_location();

    location_info(ocean); // Latitude/ Longitude: 35, 20
    location_info(tokyo); // Latitude/ Longitude: 35.68951, 139.69170
}

Dynamic Dispatch

Dynamic dispatch is used when we need to handle multiple different concrete types (mix types) and resolve the trait implementation dynamically at runtime (because this is not possible with static dispatch).

πŸ”Ž The compiler uses vtable (virtual method table) pointers. Instead of knowing the type at compile time, the program follows a pointer to a table that contains the addresses of the methods for that specific instance.

Trait Objects

struct Point { x: i32, y: i32 }
impl Addr for Point {
    fn addr(&self) -> String {
            format!("{}, {}", self.x, self.y)
    }
}

struct MapLocation(String, String); // latitude, longitude
impl Addr for MapLocation {
    fn addr(&self) -> String {
        format!("{}, {}", self.0, self.1)
    }
}

trait Addr {
    fn addr(&self) -> String;
}

// --- ⭐️ Trait Object, allow mix types and check dynamically at runtime
fn locations_info(locations: &[Box<dyn Addr>]) { // πŸ’‘ `dyn Addr` is unsized (DST) and needs a pointer
    for location in locations {
        println!("Latitude/ Longitude: {}", location.addr())
    }
}

// ---
fn main() {
    let ocean = Point { x: 35, y: 20 };
    let tokyo = MapLocation("35.68951".to_string(), "139.69170".to_string());

    let locations: Vec<Box<dyn Addr>> = vec![Box::new(ocean), Box::new(tokyo)];

    locations_info(&locations);
    // Latitude/ Longitude: 35, 20
    // Latitude/ Longitude: 35.68951, 139.69170
}

Blanket Impls (One-to-Many Manual Implementation)

Blanket impls are a special kind of trait implementation that applies to all types that have a certain type constraint/bound.

A Simple Blanket Impl

#![allow(unused)]

// --- πŸ’‘ Two types with Greet implentation
struct Person { name: String }
impl Greet for Person {}

struct Stranger {}
impl Greet for Stranger {}

trait Greet {
    const PREFIX: &'static str = "Hello";

    fn greet(&self) -> String {
        format!("{}!", String::from(Self::PREFIX))
    }
}

// --- New Farewell trait
trait Farewell {
    const PREFIX: &'static str = "Goodbye";

    fn farewell(&self) -> String {
        format!("{}!", String::from(Self::PREFIX))
    }
}

// ⭐️ Any type that implements Greet gets this Farewell implementation
impl<T: Greet> Farewell for T {} // πŸ’‘or impl<T> Farewell for T where T: Greet {}

// ---
fn main() {
    let steve = Person { name: "Steve".to_string() };
    let stranger = Stranger {};

    println!("{}", steve.greet()); // Hello!
    println!("{}", stranger.greet()); // Hello!

    println!("{}", steve.farewell()); // Goodbye!
    println!("{}", stranger.farewell()); // Goodbye!
}

Useful Blanket Impls in STD

  1. When implement Display Rust STD automatically implements ToString to the type.
use std::fmt;

struct Person {
    name: String,
}

impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Person {{ name: {}}}", self.name) // πŸ’‘ {{ }} escaping curly braces
    }
}

fn main() {
    let steve = Person {
        name: "Steve".to_string(),
    };

    println!("{steve}"); // Person { name: Steve}
    println!("{}", steve.to_string()); // Person { name: Steve}
}
  1. When implement From<T> Rust STD automatically implements Into<U> to the type.
#![allow(unused)]

use std::convert::From;

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

impl From<&str> for Person {
    fn from(name: &str) -> Self {
        Person {
            name: name.to_string(),
        }
    }
}

fn main() {
    let steve = Person::from("Steve");
    let bill: Person = "Bill".into();

    println!("{:?}", steve); // Person { name: "Steve" }
    println!("{:?}", bill); // Person { name: "Bill" }
}

πŸ‘¨β€πŸ« Try to implement TryFrom and check the blanket implementation it provides.

πŸ‘¨β€πŸ« Before going to the next…

  • πŸ’― Check Trait implementation coherence and Orphan rules.