Pack your data: Struct in Rust

Envelope for your data items, those go together get into same envelope

Amay B
CoderHack.com
8 min readSep 21, 2023

--

Photo by Mediamodifier on Unsplash

Structs are a way to create custom data types in Rust. A struct allows you to group multiple related values into a single unit.

For example, say we want to represent a user in our program. We can define a struct like this:

struct User {
username: String,
age: u8,
email: String,
}

This defines a User struct with three fields:

  • username - A String
  • age - A u8 unsigned 8 bit integer
  • email - A String

We can then create instances of this struct like this:

let user1 = User {
username: String::from("john"),
age: 30,
email: String::from("john@example.com")
};

This creates a new User struct with the given field values and binds it to the variable user1.

We can access the fields of a struct instance using dot notation:

println!("Username: {}", user1.username);
println!("Age: {}", user1.age);
println!("Email: {}", user1.email);

This will print:

Username: john 
Age: 30
Email: john@example.com

Structs are useful to represent real-world entities in your program, like users, blog posts, etc. They allow you to bundle data together in a meaningful way.

In the next section, we’ll look at derive macros which can automatically implement common traits for structs.

Derive macros used with structs

Just like envelope gets added with a stamp if you like to send in mail, Struct in rust needs Debug if you want to print its content, Serialize/Deserialize if you want to send over wire, etc.

Rust provides shorthand for implementing common traits on structs using derive attributes. These are known as derive macros.

The #[derive] attribute

The #[derive] attribute allows you to automatically generate code to implement traits for your struct. For example:

#[derive(Debug)]
struct Person {
name: String,
age: u8
}

This will automatically generate an implementation of the Debug trait for the Person struct. Basically its a macro to generate boring code of copying every attribute into output stream.

Debug

The Debug derive macro adds the std::fmt::Debug implementation for the struct. This allows you to print debug information about the struct. For example:

#[derive(Debug)] 
struct Person {
name: String,
age: u8
}

fn main() {
let john = Person {
name: String::from("John"),
age: 30
};

println!("{:?}", john);
}

This will print:

Person { name: "John", age: 30 }

Clone

The Clone derive macro adds the std::clone::Clone implementation. This allows you to explicitly clone values. For example:

#[derive(Clone)]
struct Person {
name: String,
age: u8
}

fn main() {
let john = Person {
name: String::from("John"),
age: 30
};

let clone = john.clone();
}

PartialEq/Eq

The PartialEq derive macro adds equality comparison using ==. Eq derive macro adds both == and != comparison. For example:

#[derive(PartialEq, Eq)]
struct Person {
name: String,
age: u8
}

fn main() {
let john = Person {
name: String::from("John"),
age: 30
};

let jack = Person {
name: String::from("Jack"),
age: 25
};

assert!(john == john);
assert!(john != jack);
}

When to use derive macros

The derive macros in Rust are very useful to automatically implement common traits on structs. However, there are some cases where you’ll want to implement the traits manually. Let’s go over when to use and not use the derive macros.

Use Debug for debugging

The #[derive(Debug)] macro should be used whenever you want to print out struct values for debugging purposes. The Debug trait allows a struct to be formatted using the {:?} marker. For example:

#[derive(Debug)] 
struct Point {
x: i32,
y: i32
}

fn main() {
let origin = Point { x: 0, y: 0 };
println!("{:?}", origin);
}

This will print Point { x: 0, y: 0 } - very useful for debugging!

Use Clone when you want to explicitly clone values

The #[derive(Clone)] macro should be used when you want to explicitly clone struct values. For example:

#[derive(Clone)]
struct Point {
x: i32,
y: i32
}

fn main() {
let point = Point { x: 10, y: 20 };
let cloned = point.clone();
}

This will create cloned as a clone of point.

Use PartialEq/Eq for equality comparison

The #[derive(PartialEq)] and #[derive(Eq)] macros should be used when you want to compare struct values for equality. For example:

#[derive(PartialEq, Eq)]
struct Point {
x: i32,
y: i32
}

fn main() {
let point1 = Point { x: 10, y: 20 };
let point2 = Point { x: 10, y: 20 };
assert!(point1 == point2); // Uses PartialEq
}

Here point1 and point2 are considered equal because we derived the PartialEq and Eq traits.

Use PartialOrd/Ord for ordering comparison

The #[derive(PartialOrd)] and #[derive(Ord)] macros should be used when you want to compare and sort struct values. For example:

#[derive(PartialOrd, Ord)]
struct Point {
x: i32,
y: i32
}

fn main() {
let mut points = [Point { x: 10, y: 20 },
Point { x: 5, y: 1 },
Point { x: 10, y: 20 }];
points.sort(); // Uses Ord to sort the points
}

Here we sort the array of allPoints because we derived the Ord trait.

Use Hash when you want to hash struct values

The #[derive(Hash)] macro should be used when you want to hash struct values. For example:

use std::collections::HashSet;

#[derive(Hash)]
struct Point {
x: i32,
y: i32
}

fn main() {
let point1 = Point { x: 10, y: 20 };
let point2 = Point { x: 10, y: 20 };
let set = HashSet::from_iter([point1, point2]);
}

Here we can insert bothPoints into a HashSet because we derived the Hash trait.

Use Default when you want to provide a default value

The #[derive(Default)] macro should be used when you want to provide a default value for a struct. For example:

#[derive(Default)]
struct Point {
x: i32,
y: i32
}

fn main() {
let origin = Point::default(); // x = 0, y = 0
}

Here origin will be Point { x: 0, y: 0 } because we derived Default which provides default values for the fields.

When not to use derive macros

While derive macros are convenient to quickly implement common traits for structs, there are some cases where you’ll want to implement the traits manually.

Custom behavior

If you need custom behavior for a trait implementation, you’ll have to implement it manually. For example, say you have a Rectangle struct and want to implement Debug, but you only want to print the width and height, not the origin point:

struct Rectangle {
x: i32,
y: i32,
width: i32,
height: i32,
}

impl std::fmt::Debug for Rectangle {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Rectangle - width: {}, height: {}", self.width, self.height)
}
}

Now if we print an instance of Rectangle, we'll only see the width and height:

let rect = Rectangle { x: 10, y: 5, width: 30, height: 15 }; 
println!("{:?}", rect);
// Prints "Rectangle - width: 30, height: 15"

Avoid Derive(Debug) on public API structs

For structs that are part of your public API, you may want to avoid deriving Debug. The reason for this is that the generated Debug implementation will print all fields in the struct, which could include private fields. This would expose implementation details in your debug output that you do not want to expose in your public API.

In these cases, you can implement Debug manually and print only the public fields:

pub struct APIStruct {
pub public_field: i32,
private_field: i32
}

impl std::fmt::Debug for APIStruct {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "APIStruct {{ public_field: {} }}", self.public_field)
}
}

Now the debug output will only show the public field:

let s = APIStruct { public_field: 10, private_field: 5 };
println!("{:?}", s);
// Prints "APIStruct { public_field: 10 }"

So in summary, implement traits manually when you need custom behavior or want to expose only part of a struct in your public API. Otherwise, derive macros can save you a lot of time!

Advanced struct usage

Rust structs have a few more advanced options we can explore. Let’s look at some of them.

Tuple structs

A tuple struct is a struct that has no named fields. It’s defined using a tuple-like syntax. For example:

struct Color(u8, u8, u8);

let c = Color(255, 0, 0);

This defines a struct Color with 3 unsigned 8-bit integer fields. We construct a Color value using tuple syntax, and can access the fields using dot notation:

let r = c.0;  // 255
let g = c.1; // 0
let b = c.2; // 0

Tuple structs are useful when you want to bundle together related data into a single item.

Unit structs

A unit struct is a struct with no fields at all. It’s defined using struct name followed by a semicolon:

struct Marker;

The Marker struct has no data associated with it. It's useful when you need to implement a trait on a type, but don't have any data that needs to go in the struct.

Structs with lifetimes

We can define structs that accept lifetime parameters for their fields. For example:

struct Borrowed<'a>(&'a i32);

let num = 5;
let borrowed = Borrowed(&num);

Here we define a struct Borrowed that holds a reference with lifetime 'a. We instantiate it with a reference to the num variable, and the struct adopts that lifetime.

Associated functions

We can define functions that are associated with a struct, rather than implementing them as inherent methods. For example:

struct Rectangle {
width: u32,
height: u32,
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}

fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}

Here we have an associated function square that creates a Rectangle struct. We call it like Rectangle::square(10).

Associated functions are useful for constructors, or any functionality that is related to a struct but doesn’t necessarily need to be implemented as a method.

Conclusion

In this article, we explored structs in Rust in depth. We learned:

  • Structs are used to represent records or complex data types.
  • We can define simple structs with struct keyword, fields and impl block.
struct User {
username: String,
email: String,
age: u32,
}

impl User {
fn new(username: String, email: String, age: u32) -> User {
User {
username,
email,
age
}
}
}
  • We can derive common traits on structs using #[derive] attribute. This includes:
  • Debug - for debugging
  • Clone - for explicit cloning
  • PartialEq/Eq - for equality comparison
  • PartialOrd/Ord - for ordering comparison
  • Hash - for hashing values
  • Default - for default values
  • We should avoid Derive(Debug) on public API structs.
  • We can implement traits manually when we need custom behavior.
  • There are other struct variants like:
  • Tuple structs
  • Unit structs
  • Structs with lifetimes
  • Associated functions on structs

Structs are an incredibly useful feature of Rust for modeling complex data in your programs. I hope this article helped you gain a solid understanding of structs and how to use them! Let me know if you have any other questions!

I hope this article has been helpful to you! If you found it helpful please support me with 1) click some claps and 2) share the story to your network. Let me know if you have any questions on the content covered.

Feel free to contact me at coderhack.com(at)xiv.in

--

--