Rust Traits for Beginners: A Comprehensive Guide

ajayiajayi
14 min read

Rust traits: A deep dive - LogRocket Blog

Rust is a systems programming language known for its safety, concurrency, and performance.

Rust is a fun systems programming language that has become very popular quickly because of its emphasis on concurrency, speed, and safety.

Because its special memory management architecture ensures memory safety without requiring a garbage collector, it is especially well-suited for developing concurrent, dependable, and quick applications.

Rust is one of the most promising programming languages for systems development because of its design philosophy, which combines performance and safety.

Prerequisite

It will be easier and more intuitive to learn and work with Rust if you have some basic understanding of certain topics. The following are the primary requirements to get you started with Rust:

  1. Basic Knowledge of Programming: You should have experience with at least one programming language (such as Python, Java, C++, or JavaScript) and be familiar with how to declare variables and the concept of data types (e.g., integers, strings, booleans), and how to work with them.

The concept of control flow like the if-else statements, loops (for, while) and how they work, functions, and basic algorithms (e.g. arrays, lists, etc).

  1. Understanding of Memory Management (Helpful): While not strictly required, it’s helpful to have a basic understanding of how memory works in programming, particularly if you have experience with C or C++.

Concepts of memory management like stack and heap memory, pointers and references, and manual memory management will be helpful if you want to learn and work with Rust.

  1. Familiarity with Command Line Interface (CLI): Rust development typically involves working with the command line to build and run projects, so being comfortable with them is very important in your learning curve.

    Examples of CLI tools are Basic shell commands for navigating directories, running scripts, etc. Rust’s package manager (Cargo), is used in creating, building, and managing Rust projects.

  2. Basic Version Control with Git (Optional): Many Rust projects use Git for version control, so being familiar with basic Git commands will help you manage and collaborate on Rust projects. Click this link to get started – Getting started with git.

To help you follow along with the code, all code snippets are available in GitHub Repository

What are Traits in Rust?

One of Rust's powerful features is traits. Traits enable code reusability and abstraction in a flexible and type-safe manner. In this guide, we will explore traits from the ground up, using plenty of examples and projects to help you understand how traits work.

Traits in Rust is a way to define shared behavior. It’s similar to interfaces in languages like Java or TypeScript but with more power and flexibility. Trait defines a set of methods that a type must implement. Any type that implements the trait is guaranteed to provide the behavior described by that trait.

Additionally, a trait describes the shared capability that a specific type possesses with other kinds. In an abstract sense, traits can be used to define shared behavior. Trait bounds allow us to define a generic type as any type with a given behavior.

Method signatures can be grouped to define a set of behaviors required to achieve a goal using trait definitions.

Trait Errors

When writing rust codes, errors are inevitable. Getting errors from our codes simply signifies that if we adhere to them, we will surely improve on writing error-free codes.

Errors are not to be feared but to be seen as a stepping stone for advancement in becoming an expert as a rust programmer. Let us look at the error code snippet below

The above error is a trait error that simply tells us that “not all trait items are implemented”. If you recall from the above trait code, we can see that the two methods in our traits are:

  • do_one_thing()

  • do_different_things().

But we have failed to implement them that is why the error came up. The solution to solving the error is to implement the two methods.

Basic Syntax for Defining a Trait

Now, any type that implements the Eat trait must provide an implementation for the eat method

Implementing a Trait for a Struct

Next, we implement the Eat trait for the Person struct

In the above code, we define the Eat trait with the method eat. The Person struct implements only the Eat trait.

In the main() function, we create a Person instance and call the eat method, which outputs the message indicating the person is eating.

Traits with Default Implementations

Rust allows us to provide default implementations for methods in a trait. This implies that a type implementing the trait doesn't have to define every method itself. It can rely on the default behavior unless we want to override it.

Since Robot struct uses the default say_hello implementation, we don’t have to define it ourselves.

Using Traits with Generics

Traits are often used with generics to specify that a type must implement a certain behavior. This is called trait bounds.

Trait bounds allow generic items to restrict which types and lifetimes are used as their parameters. Bounds can be provided on any type in a where clause.

From the above code, We define a generic function greet that can accept any type T as long as T implements the Greet trait.

We implement Greet for the Cat struct and use the greet function to call say_hello on a Cat instance

Understanding Trait Bounds in Rust Through a Game Example

In this example, we'll explore how Rust's trait system can be used to model a simple game with different characters (like a wizard and a ranger), and how traits define their combat abilities.

Defining Structures (Characters and Monster)

In the game scenario, we have three main types of characters: Wizard, Ranger, and Monster.

  • Wizard and Ranger are defined as player characters with a health attribute.

  • Monster is the enemy characters will fight against, also defined with a health attribute.

The #[derive(Debug)] annotation allows us to print instances of Wizard and Ranger when debugging.

Defining Traits (Abilities)

Traits in Rust are like interfaces or abstract classes in other programming languages. They allow us to define behavior that can be shared across different types.

In this case, we define three traits representing different combat abilities:

  • Magic: Characters that can perform magical attacks.

  • FightClose: Characters that can engage in close combat (e.g., sword fighting).

  • FightFromDistance: Characters that can fight from a distance (e.g., using a bow).

Implementing Traits for Characters

We implement the traits for the Wizard and Ranger based on their abilities:

  • Both the Wizard and Ranger can fight in close combat (i.e., FightClose).

  • Only the Ranger can fight from a distance (i.e., FightFromDistance).

  • Only the Wizard can use magic (i.e., Magic).

This sets the roles for each character type. For example, the Ranger can fight with both a bow and a sword, while the Wizard can fight with a sword and cast magical spells

Generic Functions with Trait Bounds

We use trait bounds to define functions that can be used by characters depending on their abilities.

  • Bow Attack: This function can be called by any character that implements both the FightFromDistance and Debug traits. The function takes a reference to the character and the opponent (the monster) and performs the attack if the distance is less than 10.

Sword Attack: This function can be used by any character that implements FightClose and Debug. It deals damage in close combat.

Fireball: This function is only available to characters that can use magic (Magic and Debug). It casts a fireball to deal more damage if the opponent is within range.

Main Function

In the main function, we create a Wizard (Gandalf) and a Ranger (Thorn), along with a Monster (Rendai). We then have the characters perform their attacks using the defined functions.

  • Thorn (the Ranger) attacks with a bow, as the distance is 8 (less than 10).

  • Gandalf (the Wizard) fights with a sword.

Gandalf can cast a fireball because the distance is within range (10).

Below snippet is the full code:

This example demonstrates how Rust’s traits and trait bounds can be used to model real-world behavior and enforce type safety.

You can create highly reusable and modular code in Rust by defining traits for different abilities and using generic functions with trait bounds.

The power of qualities in role-playing games, where characters have varying talents, is demonstrated by this example based on a game.

Trait Objects and Dynamic Dispatch

Sometimes, you want to work with multiple types that implement the same trait but don’t know the specific type at compile time.

This is where trait objects and dynamic dispatch come into play. You can use trait objects to store different types that implement the same trait.

Box<dyn Draw> is a trait object, which means we can store different types (e.g., Circle and Square) that implement the Draw trait in the same vector.

We iterate through the vector, calling the draw method dynamically.

Associated Types

Another advanced feature of traits is associated types. Associated types allow traits to declare types that are part of their contract.

In this example, the Iterator trait uses an associated type Item, which is later specified when implementing the trait for the Counter struct

Implementing Traits in Rust: A Summary Example

As we know In Rust, traits are a way to define shared behavior across different types. This example demonstrates how to implement a Summary trait for two different structures: NewsArticle and Tweet.

Summary Trait: This trait defines a method summarize that must be implemented for any type that implements the trait.

Implementing for NewsArticle: We define how a news article should be summarized by combining its headline, author, and location.

Implementing for Tweet: Similarly, we define how a tweet should be summarized using the username and the content.

Demonstration in main: We create a Tweet instance and call its summarize method to print the tweet summary.

Why We Cannot Automatically Use the Add Trait in Rust for Mixed Types

In Rust, traits like Add are used to define the behavior for the + operator. However, Rust cannot automatically derive the behavior for mixed types, such as adding an i32 and f32.

This is because Rust needs explicit instructions on how to handle the addition of different types.

Example: Why #[derive(Add)] Won’t Work

Why #[derive(Add)] Doesn't Work: The Rust compiler cannot automatically determine how to handle operations like adding an i32 and f32. It needs explicit instructions to know whether to convert one type to another or handle them differently.

Manual Add Implementation: We manually implement the Add trait for ThingsToAdd and specify the output type as f32. Inside the add method, we convert i32 to f32 using as and then perform the addition.

Custom Addition Behavior: We defined exactly how Rust should handle the addition of our custom structure, ensuring that the addition of mixed types works as expected

Implementing Traits in Rust: Example with a Canine Trait

In Rust, traits allow us to define shared behavior that different types can implement. In this example, we create a Canine trait that provides methods like bark and run.

We then implement this trait for an Animal struct and demonstrate how Rust allows us to override default trait methods when necessary.

Code Example: Defining and Implementing the Canine Trait

Implementing Custom Formatting for a Cat Struct in Rust

In Rust, the std::fmt::Display trait is used to define how a type should be formatted when printed using {}. This example demonstrates how to implement the Display trait for a custom Cat struct, and it uses custom logic to classify the cat based on its age.

Code Example: Customizing the Display Trait

Cat Struct: The Cat struct contains two fields:

  • name of type String to store the name of the cat.

  • age of type u8 (unsigned 8-bit integer) to store the cat's age.

Display Trait Implementation:

  • We implement the std::fmt::Display trait for the Cat struct, allowing us to define how an instance of Cat should be printed using {} in println!.

  • Inside the fmt function, we use a match expression to determine the "age category" (kitten, adult cat, or old cat) based on the cat's age.

  • The write! macro is used to format the final output, which combines the cat's name, age, and age category.

Output Customization

  • The custom logic in the fmt function allows for dynamic classification of the cat's age when printed. For example, a 3-year-old cat is considered an "adult cat."

Main Function:

  • In the main function, we create an instance of Cat called mr_silly with the name "Billy" and age 3.

  • We then print the instance using println!, which automatically calls the fmt method from the Display trait to format the output.

Understanding the From Trait in Rust

The From trait in Rust is used for conversions between types. It defines a generic way to convert a type into another, making type conversions smoother and more flexible. In this example, we demonstrate how the Vec::from function utilizes the From trait to convert different types into vectors.

Code Example: Using the From Trait to Create Vectors

print_vec Function:

  • This function takes a reference to a vector of generic type T and prints each element. The generic type T must implement the Display trait, which ensures that each item in the vector can be printed using {}.

Using Vec::from:

  • Array to Vector: In the first example, Vec::from([8, 9, 10]) converts an array into a vector of integers. The From trait is used to enable this conversion.

  • String Slice to Vector of Characters: In the second example, Vec::from("What kind of vector will I be?") converts a string slice (&str) into a vector of individual characters (Vec<char>). Each character in the string becomes an element in the vector.

  • String to Vector of Characters: In the third example, we first create a String and then use Vec::from to convert the String into a vector of characters. This behaves similarly to the string slice example.

Output:

  • Each vector is printed using the print_vec function. For character vectors, the characters are printed consecutively, while for the integer vector, the integers are printed in sequence without spaces.

Code Example: Implementing From for a Custom Country Type

City Struct:

  • The City struct holds two fields: name (a string representing the name of the city) and population (a 32-bit unsigned integer representing the population of the city).

  • We define a constructor new for the City struct that initializes a city with a name and population.

Country Struct:

  • The Country struct contains a Vec<City> (a vector of City instances), representing a collection of cities in the country.

From Trait Implementation:

  • We implement the From trait for the Country struct, which allows us to convert a Vec<City> directly into a Country.

  • The from method takes ownership of the Vec<City> and returns a new Country instance.

Method for Printing Cities:

  • We define a method print_cities for the Country struct that loops through its cities and prints each city's name and population.

Main Function:

  • In the main function, we create two City instances (helsinki and turku) and place them into a vector.

  • We use the From trait to convert the vector of cities into a Country.

Finally, we call print_cities to print the details of each city in the country

Implementing the From Trait for Classifying Even and Odd Numbers in Rust

This example demonstrates how to implement the From trait to transform a vector of integers into a custom struct EvenoddVec, which organizes numbers into separate vectors for even and odd numbers

EvenoddVec Struct:

  • The EvenoddVec struct is a wrapper around a Vec<Vec<i32>>, which contains two inner vectors:

    • The first inner vector holds even numbers.

    • The second inner vector holds odd numbers.

Implementing the From Trait:

  • We implement the From trait to allow the conversion of a Vec<i32> (a vector of integers) into an EvenoddVec.

  • Inside the from method:

    • We initialize even_odd_vec, a vector with two empty sub-vectors: one for even numbers and one for odd numbers.

    • We iterate through the input vector, checking if each number is even or odd. If even, it is added to the first sub-vector, otherwise to the second.

Main Function:

  • In the main function, we create a vector bunch_of_numbers containing both even and odd numbers.

  • We then use EvenoddVec::from to convert the vector into an EvenoddVec instance, which classifies the numbers into even and odd.

  • Finally, we print the contents of the EvenoddVec struct, which shows the classified numbers.

Conclusion

The From trait allows us to convert different types (like arrays, string slices, and String objects) into vectors. This demonstrates how Rust's type system and traits provide powerful abstractions, making type conversion simpler and more intuitive.

In conclusion, we have explored the powerful capabilities of Rust's trait system, focusing on how traits enable code reuse, abstraction, and flexibility.

By implementing custom traits like Summary, we can define shared behavior across different types, allowing us to write more modular and maintainable code.

We also delved into the From trait, demonstrating its use in simplifying type conversions, such as transforming a vector of integers into a custom struct.

Through practical examples, we saw how traits enhance the expressiveness of Rust's type system, offering developers a tool to build robust, type-safe abstractions while maintaining performance. Whether for converting types, enforcing shared behavior, or extending functionality, Rust’s trait system proves essential in writing concise, readable, and scalable code.

10
Subscribe to my newsletter

Read articles from ajayi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ajayi
ajayi

I, Ajayi Damola Ramon am a passionate and detail-oriented professional with a strong foundation in Statistics and expertise in Rust development. As a Blockchain Developer, I specialize in leveraging Rust to build decentralized applications and smart contracts, particularly in the ICP ecosystem. My technical journey combines deep analytical skills as a Data Analyst with hands-on coding experience, allowing me to solve complex problems through data-driven insights and efficient Rust solutions. With a background in statistics, I excel at breaking down technical concepts and translating them into actionable insights, both in the world of data science and blockchain development. I am constantly exploring new innovations in the tech world, especially Rust, Go, and blockchain technologies, and I am committed to contributing to the open-source community through projects and technical writing. Beyond coding, I am passionate about sharing knowledge through technical articles and tutorials. I regularly publish in-depth guides on Rust, blockchain, and data analytics on my Hashnode blog, helping developers of all levels deepen their understanding of emerging technologies. Connect with me to collaborate on cutting-edge blockchain projects or to explore the fascinating intersection of data, statistics, and decentralized systems.