Create your own
Lesson illustration

Implementing Traits for Shared Behavior

Hello! Welcome to our next lesson.

In our previous lessons, we've covered many of Rust's foundational features, from the ownership system to collections and slices. You've seen how accepting a slice like &[u8] can make a function more flexible than accepting a &Vec<u8>. Traits take this principle of creating flexible and abstract interfaces to a whole new level. They are Rust's primary tool for sharing behavior between different types.

Your learning outcome for this lesson is to define and implement traits to specify shared behavior for different types. Given your background in object-oriented and interface-based languages like TypeScript, you can think of traits as being similar to interfaces, but with some powerful enhancements we'll explore.

Mastering traits is crucial for your goal of Solana development. The Anchor framework, which we'll be using extensively, is built around a system of traits that automate security checks, data serialization, and account management. Understanding how they work is fundamental to writing secure and efficient Solana programs.

What is a Trait?

A trait tells the Rust compiler about functionality a type must provide. It's a contract that defines a set of method signatures. Any type that wants to conform to that contract must implement those methods.

The official Rust documentation provides a fantastic introduction to this concept. We'll start there.

Traits: Defining Shared Behavior - The Rust Programming Language

This reading from The Rust Programming Language will walk you through the basic syntax for defining a trait and then implementing it for custom structs.

Please read the sections 'Defining a Trait' and 'Implementing a Trait on a Type'. Pay close attention to: The syntax for declaring a trait with method signatures. The impl Summary for NewsArticle syntax for providing the implementation. The ability to implement the same trait for different structs (NewsArticle and SocialPost). The 'orphan rule,' which governs where you can implement traits.

To summarize the key points from your reading:

  • You define a trait with the trait keyword, followed by the method signatures.
  • You implement a trait for a type using the impl TraitName for TypeName syntax.
  • The orphan rule is a key concept: you can implement a trait on a type as long as either the trait or the type is defined within your own crate. This prevents external crates from modifying the behavior of your types in unexpected ways.

Default Implementations

Traits can also provide default implementations for their methods. This is incredibly useful for reducing boilerplate code. If a type implementing the trait doesn't provide its own specific version of a method, it will inherit the default one.

Let's continue with the Rust book to see how this works.

Traits: Defining Shared Behavior - The Rust Programming Language

One of the powerful features of traits is the ability to provide default behavior for methods. This allows you to write less code when a sensible default exists.

Read the section 'Default Implementations'. Note how an empty impl block can be used to accept the default, and how a default implementation can even call other methods within the same trait.

This feature allows for creating very rich default functionality. A trait can define complex logic in a default method that relies on a few other, simpler methods that each type must implement.

Using Traits for Generic Functions (Trait Bounds)

This is where the true power of traits becomes apparent. We can use them to write functions that accept parameters of any type, as long as that type implements a specific trait. This is known as "polymorphism."

Let's watch a video that provides a great visual walkthrough of the different syntaxes for using traits as parameters.

Understanding Traits in Rust: A Comprehensive Guide - Full New Crash Rust Tutorial for Beginners

This video by Francesco Ciulla provides a great walkthrough of the different syntaxes for using traits to make functions generic.

Watch the segment from 08:35 to 14:05. Focus on: The simple impl Trait syntax for function parameters. The equivalent generic trait bound syntax (<T: Trait>). How to specify multiple trait bounds using +. The where clause for making complex trait bounds more readable.

Let's quickly recap the syntax options for defining a function that accepts any type implementing a Loggable trait:

  1. impl Trait Syntax: Clean and concise.

    fn log_item(item: &impl Loggable) {
        // ...
    }
    
  2. Trait Bound Syntax: More verbose, but more powerful. Use this when you need to ensure multiple parameters are of the same type.

    fn log_two_items<T: Loggable>(item1: &T, item2: &T) {
        // Here, item1 and item2 are guaranteed to be the same type.
    }
    

    If you wrote fn log_two_items(item1: &impl Loggable, item2: &impl Loggable), item1 could be a User and item2 could be an Order, as long as both implement Loggable.

  3. where Clause: Best for complex or numerous trait bounds to keep the function signature readable.

    fn process_item<T>(item: &T)
    where
        T: Loggable + Clone + Send,
    {
        // ...
    }
    
Test your understanding!

You need to write a function are_equal that takes two arguments and checks if they are equal. The function should be generic, but it must guarantee that both arguments are of the same type and that this type can be compared for equality.

Rust's standard library has a trait for equality checking called PartialEq.

How would you write the function signature for are_equal?

Show answer
fn are_equal<T: PartialEq>(a: &T, b: &T) -> bool {
    a == b
}

This signature uses the trait bound syntax <T: PartialEq>. This accomplishes two things:

  1. It ensures that a and b are of the same generic type T.
  2. It constrains T to be a type that implements the PartialEq trait, which provides the == operator.

The impl PartialEq syntax would have been insufficient here, as fn are_equal(a: &impl PartialEq, b: &impl PartialEq) would allow a and b to be different types.

Common Traits and the derive Attribute

While defining your own traits is powerful, in practice, you will frequently use traits from the standard library or other crates. Many of the most common traits can be automatically implemented for your structs and enums using the #[derive] attribute.

This is a cornerstone of idiomatic Rust and is heavily used in Solana development. Let's explore some of these "must-have" traits.

5 traits your Rust types must implement

In practice, you'll spend as much time using and deriving existing traits as you will defining your own. This is especially true in Solana development. Let's look at some of the most common and important traits you'll encounter.

Watch the video, focusing on these sections: Debug (1:08 - 2:07): For printing structs to the console for debugging. Clone (1:49 - 2:24): For creating deep copies of your data. Default (2:07 - 3:34): For creating a default 'empty' instance of a struct. PartialEq (3:17 - 4:02): For comparing two instances of a struct for equality. Serialize and Deserialize (6:50 - 10:11): For converting data structures to and from a specific format. In Solana, this is fundamental for saving and loading data from accounts using a format called borsh.

The Serialize and Deserialize traits, introduced at the end of the video, are particularly vital for Solana. On-chain accounts store their data as a raw array of bytes. To work with this data in your program, you need to:

  1. Deserialize the bytes into a Rust struct when you read an account.
  2. Serialize your Rust struct back into bytes when you want to save it to an account.

The Anchor framework uses traits heavily to make this process seamless. Here is a preview of what a simple data account struct in Anchor looks like. Notice the #[derive] attribute:

// This is a preview of what you'll see in Anchor.
// Don't worry about the #[account] macro for now.
use anchor_lang::prelude::*;

#[account]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct GameState {
    pub player: Pubkey,
    pub score: u64,
    pub is_active: bool,
}

By adding #[derive(Default, Debug, Clone, PartialEq)], we are instructing the Rust compiler to automatically generate the implementations for these five common traits for our GameState struct. This saves a huge amount of boilerplate code and is a pattern you will use in every Solana program you write.

Conclusion

You have now covered one of the most important features in the Rust language. Traits provide a powerful, safe, and performant way to build abstractions and share behavior.

Here are the key takeaways from this lesson:

  • Traits Define Behavior: They act as contracts, similar to interfaces, specifying method signatures that a type must implement.
  • Default Implementations: Traits can provide default method bodies, reducing code duplication.
  • Generic Programming: Using traits as bounds (T: MyTrait or impl MyTrait) allows you to write highly flexible and reusable functions that operate on any type that exhibits the required behavior.
  • The derive Macro: For many common traits (Debug, Clone, Default, PartialEq, etc.), you can ask the compiler to generate the implementation automatically with #[derive(...)].

This lesson completes our module on "Essential Rust Concepts for Solana." You have built a strong foundation covering everything from ownership and data types to error handling and traits. You are now well-equipped to transition from general Rust programming to the specific domain of the Solana blockchain.

In our next lesson, we will begin a new module, Solana Blockchain Fundamentals. We'll start with the most critical concept you need to understand to write any Solana program: the Solana Account Model.

Can't find a good explanation? Sign up and we'll make it for you

Sign up