Rust Traits
In the Rust programming language, traits are a feature used to tell the compiler about functionality that a type must provide. Traits are used to define a standard set of behavior for different types.
Traits are most commonly used with structs, but can be used with data type.
In terms of syntax, traits are very similar to methods. A trait must first be declared using the trait keyword and then implemented using an impl block.
In this article, we will cover how to declare, implement, and use traits as well some helpful details for working with traits in more complex situations.
Declaring a Trait
Traits are declared using the trait keyword:
trait TraitName {}
Trait Naming Convention:
By convention, trait names are written using UpperCamelCase. This means that the first letter of each word in the name is capitalized and there is no separation between words.
Trait Methods
The body of the trait contains methods:
trait TraitName {
fn method_one(&self) {
// Body of method_one()
}
fn method_two(&self) {
// Body of method_two()
}
}
Concrete and Abstract Trait Methods
There are two types of trait methods: concrete and abstract.
A concrete trait method contains a body (such as in the above example). The body of the trait method contains the implementation of the trait.
In contrast, an abstract trait method does not have a body. The implementation of an abstract trait must be done using an impl block for each type.
trait TraitName {
fn concrete_trait_method(&self) {
// A concrete trait method has a body
}
fn abstract_trait_method(&self);
// The abstract trait method doesn't have a body
}
Example of Declaring a Trait
In the following example, we are declaring a trait called Greet. The Greet trait has one abstract method: say_hello() and one concrete method: say_goodbye().
// Declaring a trait called `Greet`
trait Greet {
// The say_hello() method is abstract:
fn say_hello(&self);
// The say_goodbye() method is concrete:
fn say_goodbye(&self) {
println!("Goodbye!");
}
}
Implementing a Trait
One a trait is declared, it can be implemented using an impl block. When we implement a trait, we need to specify what type it is being implemented for:
impl TraitName for Type {}
If there are any abstract trait methods that we need to implement, we can do that inside the body of the impl block. In the following example, we are implementing an abstract trait method for a struct:
impl TraitName for StructName{
fn abstract_trait_method(&self) {
// Body of abstract_trait_method()
}
}
Example of Implementing a Trait
Let’s continue to build on our example using the Greet trait.
Note that we implement the concrete trait method say_goodbye() inside the main body of the trait, but implement the abstract trait method say_hello() inside the impl block:
struct Person {
name: String,
}
trait Greet {
fn say_hello(&self);
fn say_goodbye(&self) {
println!("Goodbye!");
}
}
impl Greet for Person {
fn say_hello(&self) {
println!("Hello!");
}
}
Concrete traits can also be overridden in the impl block, which we will see below.
Using Traits
Now that we’ve learned how to declare and implement traits, let’s see how traits can be used.
In the following example, we call the trait methods directly from the main() function using method notation. Before we can call the methods however, we need an instance of the Person struct to call them on. So first we create an instance of the Person struct called bob, and then call the two trait methods on bob using a period/dot ‘.’ followed by the method name.
trait Greet {
fn say_hello(&self);
fn say_goodbye(&self) {
println!("Goodbye!");
}
}
struct Person {
name: String,
}
impl Greet for Person {
fn say_hello(&self) {
println!("Hello, {}!", self.name);
}
}
fn main() {
let bob = Person { name: String::from("Bob") };
bob.say_hello();
bob.say_goodbye();
}
Standard Output:
Hello, Bob!
Goodbye!
Using Functions to Call Trait Methods
The above example works, but what if we want to repeatedly perform complex operations using traits? In these cases, it can be much cleaner to use a function to call the trait methods.
trait Greet {
fn say_hello(&self);
fn say_goodbye(&self) {
println!("Goodbye!");
}
}
struct Person {
name: String,
}
impl Greet for Person {
fn say_hello(&self) {
println!("Hello, {}!", self.name);
}
}
fn greet_person(person: &dyn Greet) {
person.say_hello();
person.say_goodbye();
}
fn main() {
let bob = Person { name: String::from("Bob") };
let alice = Person { name: String::from("Alice") };
greet_person(&bob);
greet_person(&alice);
}
Standard Output:
Hello, Bob!
Goodbye!
Hello, Alice!
Goodbye!
In this example instead of having to call the trait methods on bob and alice individually, we instead set up the greet_person() function and passed them in as arguments.
Overriding a Default Implementation
We can override the default implementation of a concrete trait method by redeclaring the method within the body of the impl block. This is great because it allows us to have a default option but to specify unique behavior when needed:
trait Greet {
fn say_hello(&self);
fn say_goodbye(&self) {
println!("Goodbye!");
}
}
struct Person {
name: String,
}
impl Greet for Person {
fn say_hello(&self) {
println!("Hello, {}!", self.name);
}
// Overriding the default implementation of `say_goodbye`
fn say_goodbye(&self) {
println!("Goodbye, {}!", self.name);
}
}
fn greet_person(person: &dyn Greet) {
person.say_hello();
person.say_goodbye();
}
fn main() {
let bob = Person { name: String::from("Bob") };
let alice = Person { name: String::from("Alice") };
greet_person(&bob);
greet_person(&alice);
}
Standard Output:
Hello, Bob!
Goodbye, Bob!
Hello, Alice!
Goodbye, Alice!
The dyn keyword
In the above example, we needed to use the dyn keyword in the function definition of greet_person() for the code to compile correctly. The dyn keyword is a prefix of the trait object’s type and is used to ensure the safety of the trait object. How dyn works is beyond the scope of this tutorial but you can read more about it in the linked documentation.
Using Traits With Multiple Types
Up to this point, we have seen how traits can be used for a single type. However, the power of traits really comes in to play when using them for multiple types. This is one of the primary ways that Rust achieves polymorphism.
In the following example, we add a second struct called Cat, and implement the Greet trait for it. Now we can greet cats as well as people! Note that because the trait is implemented for these two structs, we can simply pass an instance of a Person or a Cat to the greeting() function, and it does the rest!
trait Greet {
fn say_hello(&self);
}
struct Person {
name: String,
}
impl Greet for Person {
fn say_hello(&self) {
println!("Hello, {}!", self.name);
}
}
struct Cat {
name: String,
}
impl Greet for Cat {
fn say_hello(&self) {
println!("Meow, {}!", self.name);
}
}
fn greeting(friend: &dyn Greet) {
friend.say_hello();
}
fn main() {
let bob = Person { name: String::from("Bob") };
let whiskers = Cat { name: String::from("Whiskers") };
greeting(&bob);
greeting(&whiskers);
}
Standard Output:
Hello, Bob!
Meow, Whiskers!