Understanding Rust's Traits: An Introduction and Examples
What Are Traits?
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 Point
s 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