Pack your data: Struct in Rust
Envelope for your data items, those go together get into same envelope
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
- AString
age
- Au8
unsigned 8 bit integeremail
- AString
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 allPoint
s 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 bothPoint
s 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 debuggingClone
- for explicit cloningPartialEq
/Eq
- for equality comparisonPartialOrd
/Ord
- for ordering comparisonHash
- for hashing valuesDefault
- 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