Now we will have our first look at how we can make our own custom types in Rust. Let's jump in!
β οΈ Remember!
You can find all the code snippets for this series in its accompanying repo
If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.β οΈβ οΈ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.
β I try to publish a new article every week (maybe more if the Rust gods π are generous π) so stay tuned π. I'll be posting "new articles updates" on my LinkedIn and Twitter.
Table of Contents:
- The basics of Struct
- Our Rectangle type
- Structs and Methods
- Associated functions that are not methods
Struct:
We have looked previously at some Rust types that groups together "related" data like Array (same type) and Tuple (different types). Basically, a Struct - or structure - is a way to group related data with different types but also naming them in a custom type. They are more or less like an Object (or more precisely, a Class) in OOP languages.
We define a named Struct with the following syntax:
Yes, there are unnamed (tuple) or even with only the struct name. but we will see those in later posts.
struct Car {
maker: String,
model: String,
year_of_making: u32,
color: String,
}
This defines a Struct called Car
that has named ... attributes - if we can call them that - and their types. This definition is located outside of any function including the main
function.
To use the Struct, we have to create an instance of it. We can then access its attributes with the dot .
notation as follows:
fn main() {
// Define and using a Struct
let car1 = Car {
model: String::from("model1"),
maker: String::from("Maker1"),
color: String::from("Red"),
year_of_making: 2023,
};
println!("Car 1 is a {} and of {} color", car1.maker, car1.color);
}
In this code snippet, we have created an immutable Car
instance and printed out its maker and color. Note the attributes order we used. The instantiation order doesn't have to match the definition order.
As Struct is considered a type, we can pass and return it to/from functions. One way we can use that is in a factory function that returns a new instance of the Struct. We can also use a neat shorthand that Rust provides that we don't have to write the name of the attribute and its variable as long as both have the same name. Meaning that instead of writing Car { maker: maker }
we can write it like that Car { maker }
which proves very usefull for Struct with many attributes:
fn main() {
.....
let car2 = get_car(
String::from("Maker2"),
String::from("Model2"),
2022,
String::from("Yellow"),
);
println!("Car 2 is a {} of {} color", car2.maker, car2.color);
}
fn get_car(maker: String, model: String, year_of_making: u32, color: String) -> Car {
Car {
maker,
model,
year_of_making,
color,
}
}
Notice the shorthand usage in the get_car
function.
We can also create Struct instances from the instances of the same type containing some or all the attributes of the original instance using the ..
syntax. But be careful, as this operation moves the original attributes if complex types (heap data) are used i.e. the new instance take ownership. When using the ..
, you can modify some of the original attributes' values and any attribute that wasn't explicitly modified will get its value from the original attributes. The ..
should be written at the end of the Struct instantiation.
fn main() {
// Define and using a Struct
let car1 = Car {
model: String::from("model1"),
maker: String::from("Maker1"),
color: String::from("Red"),
year_of_making: 2023,
};
let mut car3 = Car {
color: String::from("Blue"),
..car1
};
println!("Can we print car1 maker now? {}", car1.maker); // ERROR: borrow of moved value: `car1.maker`
println!("Car 3 is a {} and of {} color", car3.maker, car3.color);
car3.color = String::from("Green");
println!("Now car 3 is of {} color", car3.color);
}
Note that the last println!
macro will cause a compile time error as car1.maker
was moved into car3.maker
. Also, if the Struct is mutable, we can modify its values with the dot .
notation.
We can "borrow" the values and not move them between instances, but this will require the use of "lifetimes" which are a topic for another article π.
Our Rectangle type:
We will now start building our Rectangle
type (Struct). It will contain two main attributes, the width
and the height
. We can also create a function to calculate its area taking a Rectangle
as input:
struct Rectangle {
height: u32,
width: u32,
}
fn main() {
let rect1 = Rectangle {
height: 10,
width: 5,
};
println!("rect1 is {} x {} and its area is: {}", rect1.width, rect1.height, area(&rect1);
}
fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}
For this particular case, we could have used a tuple and accessed its width and height like this:
let rect1 = (5, 10);
println!("rect1 is {} x {}", rect1.0, rect1.1);
....
But the Struct one is cleaner! With the Tuple, it isn't clear which value is the width and which is the height. We rely on the order of the items in the tuple which will make our code harder to read for anyone trying to use it!
Structs and Methods:
Our area
function is very specific to the Rectangle
type. We can "associate" it with the Struct with the impl
(implementations) keyword. If we passed &self
to this function, it will become a method (Yup! this weird naming scheme again π€·ββοΈ). So, let's do that:
&self
is a shorthand forself: &Self
. TheSelf
type is an alias of the type that is in theimpl
block. We call them "associated" functions as they are "associated" with the type.
struct Rectangle {
height: u32,
width: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.height * self. width
}
}
fn main() {
let rect1 = Rectangle {
height: 10,
width: 5,
};
println!("rect1 is {} x {}", rect1.width, rect1.height);
println!("rect1 area is: {}", rect1.area());
}
We can also create methods that compare the owner instance with other instances of the same type. Like for example, we can write a method that determines if a rectangle can fit other ones comparing their width and height:
struct Rectangle {
height: u32,
width: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.height * self.width
}
fn can_hold(&self, other: &Rectangle) -> bool {
(self.width >= other.width) & (self.height >= other. height)
}
}
fn main() {
let rect1 = Rectangle {
height: 10,
width: 5,
};
println!("rect1 is {} x {}", rect1.width, rect1.height);
println!("rect1 area is: {}", rect1.area());
let rect2 = Rectangle {
height: 9,
width: 4,
};
let rect3 = Rectangle {
height: 11,
width: 6,
};
println!("Can rect1 hold rect2: {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3: {}", rect1.can_hold(&rect3));
}
With the can_hold
method, we are passing a borrow of another rectangle and see if our owner rectangle can fit those. The output will be true
and false
for the last two prints.
Associated functions that are not methods:
We can write associated functions (or methods or call them whatever you like π) in the impl
that doesn't receive &self
as a parameter (or whatever you call them). In this case, we can call them with the ::
notation (like in the String::new()
but this one is module "namespacing" which can also be used with Structs)
struct Rectangle {
height: u32,
width: u32,
}
impl Rectangle {
....
}
fn square(dimension: u32) -> Rectangle {
Rectangle {
width: dimension,
height: dimension,
}
}
}
fn main() {
let square1 = Rectangle::square(5);
println!("square1 is {} x {}", square1.width, square1.height);
}
Here, square
function is just returning a special Rectangle
with both its width and height are equal to the passed value to it.
In the next article, we will talk about how we can debug our custom types! See you then π.
Top comments (2)
I liked your article on learning Rust and exploring its struct concept as I want to know more about this programming language. Learning new skills and concepts can be challenging, but it's rewarding to see how they come together. I am always in the process of learning and I recently came across an article on usa essay writing services that can provide support for students with complex subjects and assignments. Thanks one more time for your article that gives more understanding Rust's struct.
My pleasure. I'm glad that you find it useful βΊοΈ.