Getting Started with Rust Using Rustlings — Part 6: Structs

Jesse Verbruggen
7 min readFeb 17, 2023
Part 6: Structs

In the previous part, we levelled up our understanding of Memory allocation with the concepts of Ownership, References, and Borrowing explained with the exercises on Move Semantics in Rust.

This is part six of my miniseries on learning Rust. If you want to read the previous article. You can get to it by following the link below.

In this series, I am going over my process of learning the syntax of Rust with Rustlings. In Part 1, I explain how to install Rust and Rustlings. So if you haven’t yet done that, please navigate here to follow along.

Structures

In this chapter, we’re going over Structures in Rust, called structs. In Rust, we can create three kinds of structs. Classic C Structs, Tuple Structs and Unit Structs. Structs are similar to Tuples in that both can hold multiple related values of different types. The difference here is that we name each piece of data to clarify what the values mean. This also makes structs more flexible, as we can access a single value of our structure using the names we assign.

Classic C Structs

struct Cat {
name: String,
color: String,
age: i32,
}

At home, I have three cats, Cleo, Jules and Toulouse. So I’ve defined a struct that can hold the data for all my cats. Let’s look at how we can use this struct to create objects representing my cats and print their info to the console.

fn main() {
let cleo = Cat {
name: "Cleo".to_string(),
color: "grey".to_string(),
age: 2,
};

let jules = Cat {
name: "Jules".to_string(),
color: "black".to_string(),
age: 3,
};

let toulouse = Cat {
name: "Toulouse".to_string(),
color: "ginger".to_string(),
age: 4,
};

introduce_cat(cleo);
introduce_cat(jules);
introduce_cat(toulouse);
}

fn introduce_cat(cat: Cat) {
println!(
"Meet {}, a {} year old {} cat.",
cat.name, cat.age, cat.color
);
}

In introduce_cat we are accessing the named values of our structs by using their names. We allow this function only to accept an incoming struct of type Cat and ensure that the compiler rejects any object of another type even if the fields match.

Using this struct, we can represent all my cats in code and introduce them by reusing the same function. Try to do this for all your pets and make other Structs if you have a Dog or a Fish.

Tuple Structs

struct Dice(u8);

fn main() {
let first_roll = Dice(4);
let second_roll = Dice(3);

println!("Total: {}", count_dice(&[first_roll, second_roll]));
}

fn count_dice(dices: &[Dice]) -> u16 {
dices.iter().fold(0, |acc, x| acc + u16::from(x.0))
}

Tuple Structs are defined without named values. This means we need to use the value’s index to access it. In this code, I created a Dice, which holds a value of i8 (an 8-bit signed integer). count_dice takes a reference to an array of Dice, adds them up and returns the result as an i16. We can use Tuple Structs in this manner to represent simple objects that do not require much context to understand. You can also, just like with tuples, hold multiple values in a single type to ensure functions can only take that specific type as a parameter.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

Unit Structs

struct Tree;

fn main() {
let oak = Tree;
let pine = Tree;

if oak == Tree {
println!("oak is a tree!");
}

if pine == Tree {
println!("pine is a tree!");
}
}

Unit Structs are structs that hold no values. So these take no parameters and require no braces () and no brackets {}. Unit-like structs are useful when you need to implement a trait on some type but don’t have any data you want to store in the type itself. This will be explained in a future part that explains traits in Rust.

Exercises

Before you head on and finish the exercises. Try to implement them yourself, and you can always head back to this page to compare your implementation with mine. If you found other solutions to these problems and want to share them, feel free to leave a comment below.

structs1.rs

struct ColorClassicStruct {
// TODO: Something goes here
}

struct ColorTupleStruct(/* TODO: Something goes here */);

#[derive(Debug)]
struct UnitLikeStruct;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn classic_c_structs() {
// TODO: Instantiate a classic c struct!
// let green =

assert_eq!(green.red, 0);
assert_eq!(green.green, 255);
assert_eq!(green.blue, 0);
}

#[test]
fn tuple_structs() {
// TODO: Instantiate a tuple struct!
// let green =

assert_eq!(green.0, 0);
assert_eq!(green.1, 255);
assert_eq!(green.2, 0);
}

#[test]
fn unit_structs() {
// TODO: Instantiate a unit-like struct!
// let unit_like_struct =
let message = format!("{:?}s are fun!", unit_like_struct);

assert_eq!(message, "UnitLikeStructs are fun!");
}
}

This exercise asks us to fix all of the TODO’s. Let’s tackle them one at a time and explain what’s going on.

For the first test, classic_c_structs, we need to instantiate a Classic C Struct. So let's first add the names we’re using to access the values and create a variable green representing the color green.

struct ColorClassicStruct {
red: u8,
green: u8,
blue: u8,
}

#[test]
fn classic_c_structs() {
let green = ColorClassicStruct {
red: 0,
green: 255,
blue: 0,
};

assert_eq!(green.red, 0);
assert_eq!(green.green, 255);
assert_eq!(green.blue, 0);
}

Now, let’s do the same for the Tuple Struct. This time, we don’t need named parameters.

struct ColorTupleStruct(u8, u8, u8);

#[test]
fn tuple_structs() {
let green = ColorTupleStruct(0, 255, 0);

assert_eq!(green.0, 0);
assert_eq!(green.1, 255);
assert_eq!(green.2, 0);
}

Now finally, let’s implement the Unit Struct to finish this exercise. This time, we don’t need brackets to instantiate our struct.

#[test]
fn unit_structs() {
let unit_like_struct = UnitLikeStruct;
let message = format!("{:?}s are fun!", unit_like_struct);

assert_eq!(message, "UnitLikeStructs are fun!");
}

Done, that solves this exercise. Let’s move on to the next one.

structs2.rs

#[derive(Debug)]
struct Order {
name: String,
year: u32,
made_by_phone: bool,
made_by_mobile: bool,
made_by_email: bool,
item_number: u32,
count: u32,
}

fn create_order_template() -> Order {
Order {
name: String::from("Bob"),
year: 2019,
made_by_phone: false,
made_by_mobile: false,
made_by_email: true,
item_number: 123,
count: 0,
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn your_order() {
let order_template = create_order_template();
// TODO: Create your own order using the update syntax and template above!
// let your_order =
assert_eq!(your_order.name, "Hacker in Rust");
assert_eq!(your_order.year, order_template.year);
assert_eq!(your_order.made_by_phone, order_template.made_by_phone);
assert_eq!(your_order.made_by_mobile, order_template.made_by_mobile);
assert_eq!(your_order.made_by_email, order_template.made_by_email);
assert_eq!(your_order.item_number, order_template.item_number);
assert_eq!(your_order.count, 1);
}
}

In this scenario, we are asked to use the update syntax to modify some fields of order_template with our order. The update syntax allows us to reuse values from an existing struct and overwrite only what we need. This is very useful to start from templates, as this exercise shows.

#[test]
fn your_order() {
let order_template = create_order_template();
let your_order = Order{
name: String::from("Hacker in Rust"),
count: 1,
..order_template
};

assert_eq!(your_order.name, "Hacker in Rust");
assert_eq!(your_order.year, order_template.year);
assert_eq!(your_order.made_by_phone, order_template.made_by_phone);
assert_eq!(your_order.made_by_mobile, order_template.made_by_mobile);
assert_eq!(your_order.made_by_email, order_template.made_by_email);
assert_eq!(your_order.item_number, order_template.item_number);
assert_eq!(your_order.count, 1);
}

We only had to specify a name and count for our order. All other values were collected from the order template using the ..order_template syntax.

structs3.rs

#[derive(Debug)]
struct Package {
sender_country: String,
recipient_country: String,
weight_in_grams: i32,
}

impl Package {
fn new(sender_country: String, recipient_country: String, weight_in_grams: i32) -> Package {
if weight_in_grams <= 0 {
panic!("Can not ship a weightless package.")
} else {
Package {
sender_country,
recipient_country,
weight_in_grams,
}
}
}

fn is_international(&self) -> ??? {
// Something goes here...
}

fn get_fees(&self, cents_per_gram: i32) -> ??? {
// Something goes here...
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn fail_creating_weightless_package() {
let sender_country = String::from("Spain");
let recipient_country = String::from("Austria");

Package::new(sender_country, recipient_country, -2210);
}

#[test]
fn create_international_package() {
let sender_country = String::from("Spain");
let recipient_country = String::from("Russia");

let package = Package::new(sender_country, recipient_country, 1200);

assert!(package.is_international());
}

#[test]
fn create_local_package() {
let sender_country = String::from("Canada");
let recipient_country = sender_country.clone();

let package = Package::new(sender_country, recipient_country, 1200);

assert!(!package.is_international());
}

#[test]
fn calculate_transport_fees() {
let sender_country = String::from("Spain");
let recipient_country = String::from("Spain");

let cents_per_gram = 3;

let package = Package::new(sender_country, recipient_country, 1500);

assert_eq!(package.get_fees(cents_per_gram), 4500);
assert_eq!(package.get_fees(cents_per_gram * 2), 9000);
}
}

Structs contain data, but can also have logic. In this exercise we have defined the Package struct and we want to test some logic attached to it. Make the code compile and the tests pass!

We can write functions tied to our struct with impl. new is already implemented and allows us to create constructors for our struct with custom logic assigned to it. We have to implement a body for is_international and get_fees to make this code compile.

To check if a package is international, all we need to do is compare it’s sender_country to the recipient_country. If they are not the same country, the package must cross borders and therefore is international. We must also specify that the result of this function is a boolean.

fn is_international(&self) -> bool {
self.sender_country != self.recipient_country
}

To calculate the fees of a package, we can multiply the cents_per_gram with the weight_in_grams . We also need to specify that the result of this function is an i64. Should the product of our cost and weight, which are both i32 be the highest possible number, it will still fit inside an i64.

fn get_fees(&self, cents_per_gram: i32) -> i64 {
i64::from(self.weight_in_grams) * i64::from(cents_per_gram)
}

I’m casting weight_in_grams and cents_per_gram to an i64 and multiplying them to ensure that the result is an i64. And that solves our final exercise.

I hope you enjoyed this article and managed to learn from it. Structs are the basis for complex applications, so we must know all about them before moving on to the following chapters.

The next chapter goes into detail about Rust Enums, which are more powerful than you might think.

If I helped you learn more about Rust or you found this interesting, please clap 👏 and share this article to help more people find these articles.

Let’s help each other learn and grow! Peace out✌️

--

--