Understanding Rust's Traits: An Introduction and Examples

What Are Traits?

Understanding Rust's Traits: An Introduction and Examples

What Are Traits?

In Rust, a trait is a language feature that allows you to define abstract behaviours and methods that other types can implement, making it possible to abstract over behaviour. Traits define shared behaviour that different types can have in common.

To define a trait, we use the trait keyword, followed by the trait's name and a set of method signatures defined within curly braces {}.

Let's look at an example:

trait Speak { 
    fn speak(&self); 
}

In the example above, we've defined a trait Speak that has one method speak. Any type that implements this trait must define this method.

Implementing Traits

Once we've defined a trait, we can implement that trait for any data type. To do this, we use the impl keyword, followed by the trait name for the data type.

Let's implement our Speak trait for a Dog and a Human struct.

struct Dog { 
    name: String, 
} 
 
struct Human { 
    name: String, 
} 
 
impl Speak for Dog { 
    fn speak(&self) { 
        println!("{} says: Woof!", self.name); 
    } 
} 
 
impl Speak for Human { 
    fn speak(&self) { 
        println!("{} says: Hello!", self.name); 
    } 
}

Here, we have defined two structures Dog and Human both of which have a name field. We then implemented the Speak trait for both structures with their versions of the speak method.

Using Traits

We can now make use of these traits in our functions. Here's an example:

fn make_speak<T: Speak>(t: T) { 
    t.speak(); 
} 
 
let dog = Dog { name: String::from("Fido") }; 
let human = Human { name: String::from("Alice") }; 
 
make_speak(dog);  // prints "Fido says: Woof!" 
make_speak(human); // prints "Alice says: Hello!"

In the above code make_speak is a generic function that takes any type T that implements the Speak trait. We can now pass any type that implements Speak to this function.

Default Implementations

Rust also allows us to provide default implementations for methods in our trait. This means we can let types implementing our trait use the default method or override it with their own.

trait Speak { 
    fn speak(&self) { 
        println!("Hello, I can't specify my species yet!"); 
    } 
} 
 
impl Speak for Dog { 
    // We don't provide a `speak` method here, so Dog uses the default. 
} 
 
impl Speak for Human { 
    fn speak(&self) { 
        println!("{} says: Hello, I am a human!", self.name); 
    } 
} 
 
let dog = Dog { name: String::from("Fido") }; 
let human = Human { name: String::from("Alice") }; 
 
make_speak(dog);  // prints "Hello, I can't specify my species yet!" 
make_speak(human); // prints "Alice says: Hello, I am a human!"

In this example, Dog uses the default speak method from the Speak trait, but Human provides its implementation.

Trait Bounds

Trait bounds can constrain the types used in a generic function. For instance, we can specify that the function parameter must implement a particular trait.

fn make_speak<T: Speak>(t: T) { 
    t.speak(); 
}

In this function signature, T: Speak is a trait bound that means "any type T that implements the Speak trait."

Traits as Parameters

One of the most common uses of traits is in function and method parameters. They allow functions and methods to accept parameters of different types. If you have a function that takes a trait instead of a type, it can get any type that implements it. This is a fundamental way of achieving polymorphism in Rust.

Let's see how to use a trait as a parameter:

fn say_hello(speaker: &dyn Speak) { 
    speaker.speak(); 
} 
 
let dog = Dog { name: String::from("Fido") }; 
let human = Human { name: String::from("Alice") }; 
 
say_hello(&dog);  // prints "Fido says: Woof!" 
say_hello(&human); // prints "Alice says: Hello!"

Here, say_hello accepts a reference to any type that implements the Speak trait.

Traits for Operator Overloading

Traits can also be used to overload certain operators for your types. Rust has unique traits in the standard library for overloading operators. For example, the std::ops::Add trait allows you to overload the + operator:

use std::ops::Add; 
 
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, 
        } 
    } 
} 
 
let p1 = Point { x: 1, y: 0 }; 
let p2 = Point { x: 2, y: 3 }; 
 
let p3 = p1 + p2; 
 
println!("Point: ({}, {})", p3.x, p3.y); // prints "Point: (3, 3)"

Here, we've implemented the Add trait for our Point struct, allowing us to use the + operator to add two Points together.

Traits and Inheritance

Rust doesn't have classical inheritance, unlike object-oriented languages, but you can define a trait in terms of another trait. This is a way of composing behaviours. This feature allows a trait to build upon another trait's functionality.

Here is an example:

trait Animal { 
    fn name(&self) -> String; 
} 
 
trait Speak: Animal { 
    fn speak(&self) { 
        println!("{} can't speak", self.name()); 
    } 
} 
 
impl Animal for Dog { 
    fn name(&self) -> String { 
        self.name.clone() 
    } 
} 
 
impl Speak for Dog {} 
 
let dog = Dog { name: String::from("Fido") }; 
dog.speak();  // prints "Fido can't speak"

Here, we define a base trait Animal and another trait Speak that depends on Animal. An implementation of Speak hence requires the implementation of Animal.

Check out more articles about Rust in my Rust Programming Library!

Conclusion

Rust's traits are powerful and flexible. They allow us to abstract behaviour across different types, enabling polymorphism and making our code more generic and reusable. By understanding and using traits, we can leverage Rust's type system to write safe and efficient code. While we've covered a lot of ground in this article, there is still more to explore about traits, such as trait objects, lifetime specifications in trait methods, supertraits, etc. But with this foundation, you're well-prepared to continue your journey in learning Rust.

Stay tuned, and happy coding!

Check out more articles about Rust in my Rust Programming Library!

Visit my Blog for more articles, news, and software engineering stuff!

Follow me on Medium, LinkedIn, and Twitter.

Check out my most recent book — Application Security: A Quick Reference to the Building Blocks of Secure Software.

All the best,

Luis Soares

CTO | Head of Engineering | Blockchain Engineer | Solidity | Rust | Smart Contracts | Web3 | Cyber Security

#blockchain #rust #programming #language #traits #abstract #polymorphism #smartcontracts #network #datastructures #data #smartcontracts #web3 #security #privacy #confidentiality #cryptography #softwareengineering #softwaredevelopment #coding #software

Read more