Another week, another dive into Rust. This time, we're delving into structs. Structs bear resemblance to interfaces in TypeScript, enabling the grouping of intricate data sets within an object, much like TypeScript/JavaScript. Rust also accommodates functions within these structs, offering a semblance of classes, albeit with distinctions. Let's delve into this topic.
Instantiating Structs
Structs are not too dissimilar from tuples, which we discussed a couple of posts ago, in that they hold several different values that can have different types and can be destructured before use. To define a struct, we start with the keyword struct
, followed by a name for our struct (starting with a capital letter), and describe the data between a set of curly braces.
struct Book {
title: String,
author: String,
rating: f32,
unix_release_date: i64,
}
Now that we've defined our struct, we can use it. To use the struct, we simply assign it to a variable and fill out the key: value pairs (though the order in which we do this is not important). By default, the created variable is immutable but, as usual, adding the mut keyword before the variable name will make it mutable, allowing you to set keys like so: variable_name.key_name = "some str".
let book1 = Book {
title: String::from("Learning Patterns: Patterns for Building Powerful Web Apps with Vanilla JavaScript and React"),
author: String::from("Lydia Hallie · Addy Osmani"),
rating: 4.8,
unix_release_date: 1633042800,
};
Struct instance ownership
Ownership can be transferred between struct instances by either updating a field from one instance to match that of another instance or by utilising the ..
syntax to move selected parts of the instance over.
let mut book2 = Book {
title: String::from("This is a placeholder book name."),
author: String::from("This is a placeholder author."),
..book1
};
// At this point, book1.rating and book1.unix_release_date
// have been moved to book2 and are no longer accessible
book2.author = book1.author;
// Now book1.author has been moved to book2 as well
// but book1.title is still accessible on book1
Tuple structs
Tuple structs offer an alternative way to define a struct without naming the keys explicitly. Despite their similarity to tuples, tuple structs provide predefined structures, making it easier for developers to understand the data organisation. One significant advantage is the ability to enforce type safety, ensuring that tuples with the same internal types cannot be used interchangeably in functions where they are not intended.
struct RGB(f64, f64, f64);
struct Vec3(f64, f64, f64);
fn main() {
let tomato = RGB(1.0, 0.38824, 0.27843);
let start_position = Vec3(1.74531, 6.221, 3.132234);
print_color(&tomato);
print_vector_three(&start_position);
// The following line would cause an error as it's the wrong type
// print_color(&start_position);
}
// Print the RGB values out
fn print_color(color: &RGB) {
println!("R {}, G {}, B {}", color.0, color.1, color.2);
}
// Print the vector3's XYZ values out
fn print_vector_three(vector3: &Vec3) {
println!("X {}, Y {}, Z {}", vector3.0, vector3.1, vector3.2);
}
Struct Methods
As mentioned earlier, methods make structs resemble JavaScript classes more closely than JavaScript objects. Structs may have methods, which are essentially functions added to them and can be called on an instance of a struct. We create a set of methods using the impl
keyword, which then wraps the functions. Each method must have &self
as its first parameter, which is a reference to the instance of the struct and all its data.
If we return to our book example from earlier we can add a method that will tell us if the book has more than 4 stars or not.
impl Book {
// Method to check if the book is recommended based on its rating
fn is_recommended(&self) -> bool {
// Check if the rating is greater than 4 stars
self.rating > 4.0
}
}
// We can call this with book1.is_recommended();
And if we wanted to add another parameter to our method function, we can simply add more parameter after &self
. These need types and names as usual. Let's have a look at our final book example.
struct Book {
title: String,
author: String,
rating: f32,
unix_release_date: i64,
}
impl Book {
// Method to check if the book is recommended based on its rating
fn is_recommended(&self, star_limit: f32) -> bool {
// Check if the rating is greater than the star_limit
self.rating > star_limit
}
}
const RECOMMEND_THRESHOLD: f32 = 3.5;
fn main() {
let book1 = Book {
title: String::from("Learning Patterns: Patterns for Building Powerful Web Apps with Vanilla JavaScript and React"),
author: String::from("Lydia Hallie · Addy Osmani"),
rating: 4.8,
unix_release_date: 1633042800,
};
// Call the is_recommended method on book1
let is_recommended = book1.is_recommended(RECOMMEND_THRESHOLD);
if is_recommended {
println!("This book is recommended!");
} else {
println!("This book is not recommended.");
}
}
End of week four
Thank you for joining me on this exploration. As we wrap up, remember, our Rust journey is far from over. Stay tuned for more insights as we delve deeper into its intricacies. Your feedback is invaluable, so keep it coming. And if you're on your own learning journey, share your series in the comments below. Let's keep learning together!
Thanks so much for reading. If you'd like to connect with me outside of Dev here are my twitter and linkedin come say hi 😊.
Top comments (6)
It's worth noting that this (tracking which fields are moved out) causes a great deal of complications to Rust core team so this functionality is possibly subject to change.
I didn't know that, is much of the Rust language still subject to change?
Quite so! You may look at list of tracking issues, or, for a central example, into Rustonomicon:
Everything you have written is correct
Well that is a relief ☺️
Issue:
// At this point, book1.rating and book1.unix_release_date
// have been moved to book2 and are no longer accessible
not really true, the f32 implemented Copy trait, so they are not moved to book2, just copy, you can still access book1.rating and book1.unix_release_date.