• Labs icon Lab
  • Core Tech
Labs

Guided: Intro to Traits in Rust

The purpose of this lab to introduce the fundamental concept and usages of traits in Rust. Traits promote reusability and polymorphism in Rust and will be used in this lab to develop an expense tracker application. By the end of this lab, you should have a solid understanding of how traits can typically be used in Rust and how they can add further depth and utility to your applications.

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 5m
Published
Clock icon Oct 17, 2023

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    The goal of this lab is to provide an introduction to the concept of traits in Rust. Traits are an incredibly powerful and flexible feature in Rust, with uses that include abstraction and code reusability, among other things.

    Throughout the course of this lab, you will be introduced to the definition and implementation of traits, along with some useful ways to utilize them, in the form of trait objects and bounds.

    At the end of the lab, you will incorporate your usage of traits into the main application logic of an expense tracker app to make it fully functional. You can then run the application by clicking the Run button or by entering cargo run into the Terminal.

    Feel free to also check out the solution directory if you get stuck

    info> Note: There are two files labeled mod.rs open in the editor. Hovering over each file tab will display the entire file path, helping you determine which file you need to work within.

  2. Challenge

    Step 1: Defining and Implementing Traits

    A trait in Rust is a definition for certain behavior that can be shared among multiple types. For example:

    trait MyTrait {
      fn my_method();
      fn method_two(param: i32) -> i32;
    }
    

    The above code block is a trait definition that specifies a trait called MyTrait. It defines two methods, so any type that implements this trait must provide an implementation for those two methods. In other words, any custom type or struct can implement MyTrait as long as they provide some implementation for my_method and method_two(param:i32) -> i32). You may recognize this behavior as something similar to interfaces from other languages, which is correct. In a simple way, traits are just Rust's version of interfaces.

    However, traits are special in Rust in that they are more powerful and flexible than interfaces typically are in other languages, which you'll get to later. Traits can also provide default implementations for their methods. When a default implementation is provided, any type that implements the trait does not have to provide an implementation for that method. If the method is called, the type will simply use the default implementation.

    However, you can still overwrite any default implementation with your own implementation for the type to use.

  3. Challenge

    Step 2: Implementing Traits

    Implementing traits for a type is quite simple. Syntactically it should look like the following:

    impl MyTrait for MyStruct {
      Provide method implementations here
    }
    

    Within the impl block you must provide method implementations for the trait. However, you are not required to provide an implementation for methods with default implementations unless you wish to overwrite it with your own. Now let's talk about the derive() macro. Rust essentially has a few "built-in" traits that you can utilize, one of which is Debug. By using the derive() macro to implement Debug on a custom type or struct, you can now print it using println() with {:?} debug formatting. This is a contrast to the typical {} display format, which requires manual implementation of the Display trait for custom types. While Debug won't be as pretty as Display, it provides a quick and simple solution for development when you just want to see the contents of a type.

    The macro is usually called as follows:

    #[derive(Debug)]
    struct MyStruct
    

    You can also call multiple of Rust's "built-in" traits besides Debug as parameters for derive().

    The derive() macro will be useful here because you will need to implement the Debug trait in order to implement the print_debug() method for Expense.

  4. Challenge

    Step 3: Trait Objects

    A key theme with usage of traits is reusability and shared behavior. There may be times where you want to work with the functionality of a trait, regardless of the types that implement the trait. This is where trait objects come into play.

    A trait object is a way to work with values of different concrete types that implement a common trait. To handle allocation and specific implementations, Rust employs a mechanism known as dynamic dispatch to determine the actual type at runtime in conjunction with a smart pointer known as Box<> to point to the type on the heap. Here is an example:

    trait MyTrait {
     code here
    }
    
    struct FirstStruct {
      code here
    }
    
    struct SecondStruct {
       code here
    }
    
    impl MyTrait for FirstStruct {
       code here
    }
    
    impl MyTrait for SecondStruct {
       code here
    }
    
    struct ThirdStruct {
     my_field: Box<dyn MyTrait>
    }
    

    In this example, you have a trait defined as MyTrait. Both FirstStruct and SecondStruct implement MyTrait, even if they may have different implementations for the trait. Any instance of both FirstStruct or SecondStruct would be a valid value for my_field in ThirdStruct, since both of them implement MyTrait. Any type that doesn't implement MyTrait however, could not be set as a value for my_field.

  5. Challenge

    Step 4: Trait Bounds

    Traits can also be used with generics in the form of trait bounds. Take a look at the following examples:

    fn func_one<T: MyTrait>(P: &T) {
      method body here
    }
    
    fn func_one<T: MyTrait>() -> T {
      method body here
    }
    

    Both of these generic methods utilize trait bounds, which specify that T must be a type that implements MyTrait.

    This is useful in your application if you look in main.rs. The function get_input() currently only returns i32, but you would have to make multiple versions of get_input() if you also want it to retrieve strings or floats. To remedy this, you can make get_input() a generic method so it can return any of those types using the parse() method. You can specify a std::str::FromStr trait bound for these types to ensure that the types get_input returns are parsable from strings. After completing these tasks, the expense tracker app is now complete. Running the application will now give you a menu that lets you add new expenses to your expense tracker or list all the expenses in the tracker. Listing all expenses will also display a total expense value that is a sum of all the current expenses.

    Completing this lab should have given or reinforced a fundamental understanding of traits in Rust as well as given you an idea on some of the ways they can be utilized to supplement your code.

George is a Pluralsight Author working on content for Hands-On Experiences. He is experienced in the Python, JavaScript, Java, and most recently Rust domains.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.