Mastering Rust: Traits Explained

In the Rust programming language, a trait is a very important concept that can include: functions, constants, types, etc.

To put it simply, a trait defines shared behavior in an abstract way. It can be thought of as an interface in some languages, but there are certain differences, which will be introduced below.

1. Member Methods

Methods can be defined within a trait.

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

We defined a method area in a trait named Shape.

1.1 Method Parameters

In the definition of Shape, the method parameter is &self.

In fact, every trait has a hidden type Self (with an uppercase S), which represents the specific type that implements this trait.

In Rust, Self and self are both keywords; uppercase Self is a type name, while lowercase self is a variable name.

In fact, area(&self) is equivalent to area(self: &Self), but Rust provides a simplified syntax.

The following cases are all equivalent.

trait T {
    fn method1(self: Self);
    fn method2(self: &Self);
    fn method3(self: &mut Self);
}
// Equivalent to the method definitions below
trait T {
    fn method1(self);
    fn method2(&self);
    fn method3(&mut self);
}

1.2 Calling Examples

Refer to the following example:

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

struct Circle {
    radius: f64,
}

impl Shape for Circle {
    // Self's type is Circle
    fn area(self: &Self) -> f64 {
        // Access member variable through self.radius
        std::f64::consts::PI * self.radius * self.radius
    }
}

fn main() {
    let circle = Circle { radius: 2f64 };
    println!("The area is {}", circle.area())
}

1. Access member variables through self.member_variable;

2. Call member methods through instance.member_method;

2. Anonymous Traits

impl Circle {
    fn get_radius(&self) -> f64 {
        self.radius
    }
}

The impl keyword is followed directly by the type, without the name of the trait.

The above code can be seen as implementing an anonymous trait for Circle.

3. Static Methods

Static methods: methods where the first parameter is not a self parameter.

impl Circle {
    // Regular method
    fn get_radius(&self) -> f64 {
        self.radius
    }

    // Static method
    fn get_area(this: &Self) -> f64 {
        std::f64::consts::PI * this.radius * this.radius
    }
}

fn main() {
    let c = Circle { radius: 2f64 };
    // Call regular method
    println!("The radius is {}", c.radius);
    // Call static method
    println!("The area is {}", Circle::get_area(&c))
}

Note the differences from regular methods, such as parameter naming and calling methods (regular methods are called with instance.method, while static methods are called with Type::method).

Static methods can be called using Type::FunctionName().

4. Extension Methods

Using traits to add methods to other types.

For example, we can add a method to the built-in type i32:

// Extension method
trait Double {
    fn double(&self) -> Self;
}
impl Double for i32 {
    fn double(&self) -> i32 {
        self * 2
    }
}

fn main() {
    let x: i32 = 10.double();
    println!("x double is {}", x); // 20
}

5. Generic Constraints

In Rust, static dispatch and dynamic dispatch are two different mechanisms used to select and call functions.

5.1 Static Dispatch

Determines the specific implementation of a function call at compile time.

It achieves efficient calls by resolving function calls and selecting the correct function implementation during the compilation phase.

Static dispatch is typically used in situations where generics are involved, allowing the compiler to determine the function to call based on specific type parameters.

fn main() {
    fn myPrint<T: ToString>(v: T) {
        v.to_string();
    }
    
    let c = 'a';
    let s = String::from("hello");
    
    myPrint::<char>(c);
    myPrint::<String>(s);
}

Equivalent to:

fn myPrint(c: char) {
    c.to_string();
}
fn myPrint(str: String) {
    str.to_string();
}

5.2 Dynamic Dispatch

Selects the implementation of a function based on the actual type of the object at runtime.

It is applicable in situations where trait objects (using the dyn keyword) are used, where the compiler cannot determine the specific function implementation during the compilation phase.

At runtime, the program dynamically selects the function to call based on the actual type contained in the trait object.

Dynamic dispatch provides greater flexibility but may incur some runtime overhead compared to static dispatch.

The following code demonstrates the differences between static and dynamic dispatch:

trait Animal {
    fn make_sound(&self);
}

struct Cat;
struct Dog;

impl Animal for Cat {
    fn make_sound(&self) {
        println!("Meow!");
    }
}

impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
}

fn static_dispatch(animal: &impl Animal) {
    animal.make_sound();
}

fn dynamic_dispatch(animal: &dyn Animal) {
    animal.make_sound();
}

fn main() {
    let cat = Cat;
    let dog = Dog;

    // Static dispatch
    static_dispatch(&cat);
    static_dispatch(&dog);

    // Dynamic dispatch
    dynamic_dispatch(&cat as &dyn Animal);
    dynamic_dispatch(&dog as &dyn Animal);
}

6. Consistency Principle

The consistency principle, also known as the orphan rule:

Impl blocks must either be declared in the same crate as the trait block or in the same crate as the type declaration.

The orphan rule is an important design principle in the Rust language that helps ensure the controllability and traceability of trait implementations. Adhering to the orphan rule can improve code readability and maintainability while reducing potential conflicts and confusion.

In other words, if a trait comes from an external source and the type also comes from an external crate, the compiler does not allow you to implement this trait for that type. At least one of them must be defined in the current crate.

For example, the following two cases are both valid:

use std::fmt::Display;

struct A;
impl Display for A {}
trait TraitA {}
impl TraitA for u32 {}

However, the following case is not allowed:

use std::fmt::Display;

impl Display for u32 {}
Mastering Rust: Traits Explained

This also provides us with a standard: when upstream developers write libraries, they should provide commonly used standard traits, such as Display, Debug, ToString, Default, etc., as much as possible.

Otherwise, downstream developers using this library will not be able to help us implement these traits.

7. Differences Between Traits and Interfaces

At the beginning, we mentioned that to facilitate understanding of traits, they can be imagined as interfaces in other languages, such as Java. However, there are significant differences.

Rust is a strongly typed language that allows users precise control over memory. In the current version of Rust, it is stipulated that:

The types of function parameters, return values, etc., must have their sizes determined at compile time.

However, traits themselves are neither concrete types nor pointer types; they only define abstract constraints on types. Different types can implement the same trait, and types that satisfy the same trait may have different sizes.

Therefore, traits do not have a fixed size at compile time, and we cannot directly use traits as instance variables, parameters, or return values.

Similar to the following code snippets are incorrect:

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

impl Circle {
    // Error 1: trait (Shape) cannot be a parameter type
    fn use_shape(arg: Shape) {
    }
    // Error 2: trait (Shape) cannot be a return value type
    fn ret_shape() -> Shape {
    }
}
fn main() {
    // Error 3: trait (Shape) cannot be a local variable type
    let x: Shape = Circle::new();
}

We can see the compiler’s error messages:

Mastering Rust: Traits Explained

8. Derive

The Rust standard library implements some traits with fixed logic internally, and through the derive attribute, we can automatically implement certain traits without manually writing the corresponding code.

#[derive(Debug)]
struct Foo {
    data: i32,
}
fn main() {
    let v1 = Foo { data: 0 };
    println!("{:?}", v1)
}

Adding the Debug trait implementation facilitates formatted printing of the struct.

#[derive(Debug)] is equivalent to impl Debug for Foo {}

Currently, the traits that Rust supports for automatic deriving include:

Copy, Clone, Default, Hash,
Debug, PartialEq, Eq, PartialOrd,
Ord, RustcEncodable, RustcDecodable,
FromPrimitive, Send, Sync

9. Common Traits in the Standard Library

In introducing derive, we mentioned some built-in traits, which are common in the standard library. Below, we will introduce what these traits do.

9.1 Display and Debug

Let’s look at the source definitions:

Display

pub trait Display {
    /// Formats the value using the given formatter.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::fmt;
    ///
    /// struct Position {
    ///     longitude: f32,
    ///     latitude: f32,
    /// }
    ///
    /// impl fmt::Display for Position {
    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    ///         write!(f, "({}, {})", self.longitude, self.latitude)
    ///     }
    /// }
    ///
    /// assert_eq!("(1.987, 2.983)",
    ///            format!("{}", Position { longitude: 1.987, latitude: 2.983, }));
    /// ```
    #[stable(feature = "rust1", since = "1.0.0")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

Debug

pub trait Debug {
    /// Formats the value using the given formatter.
    ///
    /// # Examples
    ///
    /// ```
    /// use std::fmt;
    ///
    /// struct Position {
    ///     longitude: f32,
    ///     latitude: f32,
    /// }
    ///
    /// impl fmt::Debug for Position {
    ///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    ///         f.debug_tuple("")
    ///          .field(&self.longitude)
    ///          .field(&self.latitude)
    ///          .finish()
    ///     }
    /// }
    ///
    /// let position = Position { longitude: 1.987, latitude: 2.983 };
    /// assert_eq!(format!("{:?}", position), "(1.987, 2.983)");
    ///
    /// assert_eq!(format!("{:#?}", position), "(
    ///     1.987,
    ///     2.983,
    /// )");
    /// ```
    #[stable(feature = "rust1", since = "1.0.0")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}

1. Only types that implement the Display trait can be printed using the {} format.

2. Only types that implement the Debug trait can be printed using {:?} and {:#?} formats.

The differences between the two are as follows:

1. Display assumes that this type can be represented as a UTF-8 formatted string, intended for end-users, and not all types should or can implement this trait. The formatting of the string is entirely up to the programmer, and the compiler does not provide automatic deriving functionality for this trait.

2. Another commonly used trait in the standard library is std::string::ToString, which is automatically implemented for all types that implement the Display trait. It includes a method to_string(&self) -> String. Any type that implements the Display trait can call the to_string() method to format a string.

3. Debug is primarily for debugging purposes, and it is recommended that all “public” types as APIs implement this trait for easier debugging. The string printed is not based on “aesthetic readability”; the compiler provides automatic deriving functionality for this trait.

struct Color {
    r: u8,
    g: u8,
    b: u8,
}

impl Default for Color {
    fn default() -> Self {
        Self { r: 0, g: 0, b: 0 }
    }
}

Equivalent to:

#[derive(Default)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

9.2 ToString

<span>ToString</span> is a very commonly defined trait in the Rust standard library, aimed at converting any type that implements it into a String type representation.

#[cfg_attr(not(test), rustc_diagnostic_item = "ToString")]
#[stable(feature = "rust1", since = "1.0.0")]
pub trait ToString {
    /// Converts the given value to a `String`.
    ///
    /// # Examples
    ///
    /// ```
    /// let i = 5;
    /// let five = String::from("5");
    ///
    /// assert_eq!(five, i.to_string());
    /// ```
    #[rustc_conversion_suggestion]
    #[stable(feature = "rust1", since = "1.0.0")]
    #[cfg_attr(not(test), rustc_diagnostic_item = "to_string_method")]
    fn to_string(&self) -> String;
}

Automatic Implementation

Although <span>ToString</span> is a trait, you almost never need to implement it manually, because the standard library has already automatically implemented it for all types that implement <span>Display</span>.

In other words:

If you implement <span>Display</span> ⇒ you automatically have <span>.to_string()</span> method.

<span>to_string()</span> is essentially equivalent to <span>format!("{}", value)</span>.

#[cfg(not(no_global_oom_handling))]
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: fmt::Display + ?Sized> ToString for T {
    #[inline]
    fn to_string(&self) -> String {
        <Self as SpecToString>::spec_to_string(self)
    }
}

impl<T: fmt::Display + ?Sized> SpecToString for T {
    // A common guideline is to not inline generic functions. However,
    // removing `#[inline]` from this method causes non-negligible regressions.
    // See <https://github.com/rust-lang/rust/pull/74852>, the last attempt
    // to try to remove it.
    #[inline]
    default fn spec_to_string(&self) -> String {
        let mut buf = String::new();
        let mut formatter =
            core::fmt::Formatter::new(&mut buf, core::fmt::FormattingOptions::new());
        // Bypass format_args!() to avoid write_str with zero-length strs
        fmt::Display::fmt(self, &mut formatter)
            .expect("a Display implementation returned an error unexpectedly");
        buf
    }
}

9.3 PartialEq/Eq

In Rust, <span>PartialOrd</span>, <span>Ord</span>, <span>PartialEq</span>, and <span>Eq</span> are traits used for comparison and sorting. By using the <span>derive</span> macro, we can automatically implement the default behavior of these traits for structs or enums.

Here is a brief explanation of these traits:

  1. <span>PartialOrd</span> trait: Used for partial ordering comparisons, meaning it can be compared but not necessarily fully sorted. It defines the <span>partial_cmp</span> method, which compares two values and returns an <span>Option<Ordering></span> enum indicating the comparison result.
  2. <span>Ord</span> trait: Used for total ordering comparisons, meaning it can be fully sorted. It is a superset of the <span>PartialOrd</span> trait and defines the <span>cmp</span> method, which compares two values and returns an <span>Ordering</span> enum indicating the comparison result.
  3. <span>PartialEq</span> trait: Used for partial equality comparisons. It defines methods such as <span>eq</span>, <span>ne</span>, <span>lt</span>, <span>le</span>, <span>gt</span>, and <span>ge</span> for comparing two values for equality, inequality, less than, less than or equal to, greater than, and greater than or equal to.
  4. <span>Eq</span> trait: Used for total equality comparisons, meaning it can determine complete equality. It is a superset of the <span>PartialEq</span> trait and does not require manual implementation; it automatically derives from implementing the <span>PartialEq</span> trait.

Eq is defined as a subtrait of PartialEq.

#[derive(PartialEq, Debug)]    // Note this line
struct Point {
    x: i32,
    y: i32,
}
fn example_assert(p1: Point, p2: Point) {
    assert_eq!(p1, p2);        // Comparison
}

9.4 PartialOrd/Ord

PartialOrd is similar to PartialEq; PartialEq only checks for equality or inequality, while PartialOrd further checks for less than, less than or equal to, greater than, or greater than or equal to. It is designed for sorting functionality.

PartialOrd is defined as a subtrait of PartialEq. They can be derived together using procedural macros.

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

#[derive(PartialEq, PartialOrd)]
enum Stoplight {
    Red,
    Yellow,
    Green,
}

9.5 Clone

This trait provides the target type with a <span>clone()</span> method to completely clone instances.

#[stable(feature = "rust1", since = "1.0.0")]
#[lang = "clone"]
#[rustc_diagnostic_item = "Clone"]
#[rustc_trivial_field_reads]
pub trait Clone: Sized {
    /// Returns a copy of the value.
    ///
    /// # Examples
    ///
    /// ```
    /// # #![allow(noop_method_call)]
    /// let hello = "Hello"; // &str implements Clone
    ///
    /// assert_eq!("Hello", hello.clone());
    /// ```
    #[stable(feature = "rust1", since = "1.0.0")]
    #[must_use = "cloning is often expensive and is not expected to have side effects"]
    // Clone::clone is special because the compiler generates MIR to implement it for some types.
    // See InstanceKind::CloneShim.
    #[lang = "clone_fn"]
    fn clone(&self) -> Self;

    /// Performs copy-assignment from `source`.
    ///
    /// `a.clone_from(&b)` is equivalent to `a = b.clone()` in functionality,
    /// but can be overridden to reuse the resources of `a` to avoid unnecessary
    /// allocations.
    #[inline]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn clone_from(&mut self, source: &Self) {
        *self = source.clone()
    }
}

From the method signature, we can see that the method uses an immutable reference of the instance.

fn clone(&self) -> Self;

For example:

#[derive(Clone)]
struct Point {
    x: u32,
    y: u32,
}

Since each field (of type u32) implements Clone, the derive automatically implements the Clone trait for the Point type. After implementation, the instance point can use point.clone() to clone itself.

Note: clone() is a deep copy of the object, which may incur significant overhead, but in most cases, it is acceptable. Don’t worry about using clone() in Rust; getting the program functionality to work is the most important thing. Rust code generally performs well, as it has a high starting point.

9.6 Copy

#[rustc_unsafe_specialization_marker]
#[rustc_diagnostic_item = "Copy"]
pub trait Copy: Clone {
    // Empty.
}

Defined as a subtrait of Clone, and it does not contain any content; it is merely a marker.

The Rust standard library provides a Copy procedural macro that allows us to automatically implement the Copy trait for the target type.

9.7 ToOwned

ToOwned is a broader version of Clone. It provides a <span>to_owned()</span> method that can convert a reference into an owned instance.

let a: &str = "123456";
let s: String = a.to_owned();

9.8 Drop

The Drop trait is used to provide custom garbage collection (cleanup) for types.

trait Drop {
    fn drop(&mut self);
}

Instances of types that implement this trait trigger the drop() method when they go out of scope, which occurs before the instance is destroyed.

#[derive(PartialEq, Debug, Clone)]    // Note this line
struct Point {
    x: i32,
    y: i32,
}

impl Drop for Point {
    fn drop(&mut self) {
        println!("Dropping point ({},{})", self.x, self.y);
    }
}
fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p);
}

Output:

Mastering Rust: Traits Explained

Generally, we do not need to implement this trait for our types unless we encounter special situations, such as when we need to call external C library functions that allocate resources, and the C library functions are responsible for releasing them. In this case, we need to implement Drop on the Rust wrapper type (for the type in the C library) and call the resource release function from that C library.

9.9 From<T> and Into<T>

These two traits are used for type conversion.

<span>From<T></span> can convert type T into itself, while <span>Into<T></span> can convert itself into type T.

trait From<T> {
    fn from(T) -> Self;
}
trait Into<T> {
    fn into(self) -> T;
}

As you can see, they are inverse traits. In fact, Rust only allows us to implement <span>From<T></span>, because implementing <span>From<T></span> automatically implements <span>Into<T></span>. Please see the implementation in the standard library.

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

9.10 TryFrom and TryInto

<span>TryFrom<T></span> and <span>TryInto<T></span> are the fallible versions of <span>From<T></span> and <span>Into<T></span>. If you think the conversion may fail, choose these two traits to implement.

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

trait TryInto<T> {
    type Error;
    fn try_into(self) -> Result<T, Self::Error>;
}

As you can see, calling <span>try_from()</span> and <span>try_into()</span> returns a <span>Result</span>, and you need to handle the <span>Result</span>.

9.11 FromStr

Converts from string type to itself.

trait FromStr {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

For example, the <span>parse()</span> method of strings:

use std::str::FromStr;

fn example<T: FromStr>(s: &str) {
    // The following four expressions are equivalent
    let t: Result<T, _> = FromStr::from_str(s);
    let t = T::from_str(s);
    let t: Result<T, _> = s.parse();
    let t = s.parse::<T>(); // Most commonly used syntax
}

9.12 as_ref

trait AsRef<T> {
    fn as_ref(&self) -> &T;
}

It converts a reference of itself into a reference of the target type. The difference from <span>deref()</span> is that <span>as_ref()</span> needs to be called explicitly, making the code clearer and reducing the chance of errors.

<span>AsRef<T></span> allows for more diverse types to be passed in function parameters, whether they are reference types or owned types. For example:

// Using &str as a parameter can accept the following two types
//  - &str
//  - &String
fn takes_str(s: &str) {
    // use &str
}
// Using AsRef<str> as a parameter can accept the following three types
//  - &str
//  - &String
//  - String
fn takes_asref_str<S: AsRef<str>>(s: S) {
    let s: &str = s.as_ref();
    // use &str
}
fn example(slice: &str, borrow: &String, owned: String) {
    takes_str(slice);
    takes_str(borrow);
    takes_str(owned); // ❌
    takes_asref_str(slice);
    takes_asref_str(borrow);
    takes_asref_str(owned); // ✅
}

In this example, an owned String can also be passed directly as a parameter, expanding the expression of parameter types compared to &str.

You can think of Deref as an implicit (or automated) + weakened version of AsRef<T>.

Leave a Comment