Alex Merced I am a developer, educator, and founder of devNursery.com.

Fundamentals for using structs in Rust 

5 min read 1497

Fundamentals Rust Structs

What are structs?

Understanding object-oriented programming is a must for any developer. Object-oriented programming involves creating classes, which act as descriptions or blueprints of an object. The object is typically made up of several variables or functions.

In languages like C, Go, and Rust, classes are not a feature. Instead, these languages use structs, which define only a group of properties. While structs don’t allow you to define methods, both Rust and Go define functions in a way that provides access to structs.

In this tutorial, we’ll learn the basics of how structs operate in Rust. Let’s get started!

What is Rust?

Rust, a programming language created by Mozilla, fills a similar role to C by being a fast, low-level language that uses modern syntax patterns and a central package manager.

Writing a struct in Rust

In the code below, we’ll write a simple struct for a Cat type that includes the properties name and age. Once we define our struct, we’ll define our main function.

We’ll create a new string and a new instance of the struct, passing it the name and age properties. We’ll print the entire struct and interpolate its properties in a string. For name, we’ll use Scratchy. For age, we’ll use 4:

// This debug attribute implements fmt::Debug which will allow us
// to print the struct using {:?}
#[derive(Debug)]
// declaring a struct
struct Cat {
 // name property typed as a String type
 name: String,
 // age typed as unsigned 8 bit integer
 age: u8
}
fn main() {
 // create string object with cat's name
 let catname = String::from("Scratchy");
 // Create a struct instance and save in a variable
 let scratchy = Cat{ name: catname, age: 4 };

Note that we’re using the derive attribute, which we’ll cover in detail later, to automate the implementation of certain traits on our struct. Since we derive the debug trait, we can print the entire struct using {:?}:

// using {:?} to print the entire struct
 println!("{:?}", scratchy);

 // using individual properties in a String
 println!("{} is {} years old!", scratchy.name, scratchy.age);
}

There are several important things to note in this section. First, as with any value in Rust, each property in the struct must be types. Additionally, be sure to consider the difference between a string (a string object or struct) and &str (a pointer to a string). Since we’re using the string type, we have to create a string from a proper string literal.

The derive attribute

By default, structs aren’t printable. A struct must implement the stc::fmt::debug function to use the {:?} formatter with println!. However, in our code example above, we used the derive(Debug) attribute instead of implementing a trait manually. This attribute allows us to print out structs for easier debugging.

We made a custom demo for .
No really. Click here to check it out.

Attributes act as directives to the compiler to write out the boilerplate. There are several other built in derive attributes in Rust that we can use to allow the compiler to implement certain traits for us:

  • [#derive(hash)]: converts the struct into a hash
  • [#derive(clone)]: adds a clone method to duplicate the struct
  • [#derive(eq)]: implements the eq trait, setting equality as all properties having the same value

Struct traits

We can create a struct with properties, but how can we tie them to functions as we do classes in other languages?

Rust uses a feature called traits, which define a bundle of functions for structs to implement. One benefit of traits is you can use them for typing. You can create functions that can be used by any structs that implement the same trait. Essentially, you can build methods into structs as long as you implement the right trait.

Using traits to provide methods allows for a practice called composition, which is also used in Go. Instead of having classes that typically inherit methods from one parent class, any struct can mix and match the traits that it needs without using a hierarchy.

Writing a trait

Let’s continue our example from above by defining a Cat and Dog struct. We’d like for both to have a Birthday and Sound function. We’ll define the signature of these functions in a trait called Pet.

In the example below, we’ll use Spot as the name for Dog. We use goes Ruff as Sound and 0 for age. For Cat, we’ll use goes Meow as the sound and 1 for age. The function for birthday is self.age += 1;:

// Create structs
#[derive(Debug)]
struct Cat { name: String, age: u8 }
#[derive(Debug)]
struct Dog { name: String, age: u8 }
// Declare the struct
trait Pet {
 // This new function acts as a constructor
 // allowing us to add additional logic to instantiating a struct
 // This particular method belongs to the trait
 fn new (name: String) -> Self;
// Signature of other functions that belong to this trait
 // we include a mutable version of the struct in birthday
 fn birthday(&mut self);
 fn sound (&self);
}

// We implement the trait for cat
// we define the methods whose signatures were in the trait
impl Pet for Cat {

 fn new (name: String) -> Cat {
   return Cat {name, age: 0};
 }

 fn birthday (&mut self) {
   self.age += 1;
   println!("Happy Birthday {}, you are now {}", self.name, self.age);
 }

 fn sound(&self){
   println!("{} goes meow!", self.name);
 }
}

// We implement the trait for dog
// we only define sound. Birthday and name are already defined
impl Pet for Dog {

 fn new (name: String) -> Dog {
   return Dog {name, age: 0};
 }

 fn birthday (&mut self) {
   self.age += 1;
   println!("Happy Birthday {}, you are now {}", self.name, self.age);
 }

 fn sound(&self){
   println!("{} goes ruff!", self.name);
 }
}

Notice we define a new method that acts like a constructor. Instead of creating a new Cat like we did in our previous snippet, we can just type our new variable!

When we invoke the constructor, it will use the new implementation of that particular type of struct. Therefore, both Dog and Cat will be able to use the Birthday and Sound functions:

fn main() {
 // Create structs using the Pet new function
 // using the variable typing to determine which
 // implementation to use
 let mut scratchy: Cat = Pet::new(String::from("Scratchy"));
 let mut spot: Dog = Pet::new(String::from("Spot"));

 // using the birthday method
 scratchy.birthday();
 spot.birthday();

 // using the sound method
 scratchy.sound();
 spot.sound();
}

There are several important things to note about traits. For one, you must define the function for each struct that implements the trait. You can do so by creating default definitions in the trait definition.

We declared the structs using the mut keyword because structs can be mutated by functions. For example, birthday increments age and mutates the properties of the struct, therefore, we passed the parameter as a mutable reference to the struct (&mut self).

In this example, we used a static method to initialize a new struct, meaning the type of the new variables is determined by the type of struct.

Returning a struct

Sometimes, a function may return several possible structs, which occurs when several structs implement the same trait. To write this type of function, just type the return value of a struct that implements the desired trait.

Let’s continue with our earlier example and return Pet inside a Box object:

// We dynamically return Pet inside a Box object
fn new_pet(species: &str, name: String) -> Box<dyn Pet> {

In the example above, we use the Box type for the return value, which allows us to allocate enough memory for any struct implementing the Pet trait. We can define a function that returns any type of Pet struct in our function as long as we wrap it in a new Box.

We create a function that instantiates our Pet without specifying age by passing a string of the type Pet and name. Using if statements, we can determine what type of Pet to instantiate:

if species == "Cat" {
   return Box::new(Cat{name, age: 0});
 } else {
   return Box::new(Dog{name, age: 0});
 }
}

The function returns a Box type, which represents memory being allocated for an object that implements Pet. When we created Scratchy and Spot, we no longer had to type the variables. We laid out the logic explicitly in the function where a Dog or Cat would be returned:

fn main() {
 // Create structs using the new_pet method
 let mut scratchy = new_pet("Cat", String::from("Scratchy"));
 let mut spot = new_pet("Dog", String::from("Spot"));

 // using the birthday method
 scratchy.birthday();
 spot.birthday();

 // using the sound method
 scratchy.sound();
 spot.sound();
}

Summary

We have learned the following about structs in Rust:

  1. Structs allow us to group properties in a single data structure
  2. Using traits, we can implement different methods on a struct
  3. Typing with traits allows us to write functions that can receive and return structs
  4. The derive attribute allows us to implement certain traits in our structs with ease

Now, we can implement typical object-oriented design patterns in Rust using composition over inheritance.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Alex Merced I am a developer, educator, and founder of devNursery.com.

Leave a Reply