Table of Contents
Structs
Structs in Rust are similar to structs in other programming languages. They allow you to create custom data types by grouping related data together.
Rust has Three types of structs:
- Named field Struct
- Tuple Struct
- Unit Struct
RUST struct examples
Named Field Struct
You can define a struct using the struct keyword followed by the name of the struct and a block of code that defines the fields of the struct.
struct Person {
name: String,
age: u32,
address: String,
}
In this example, we have defined a Person struct that has three fields: name, age, and address. Each field has a specific data type associated with it.
struct Person {
name: String,
age: u32,
address: String,
}
fn main () {
let john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
}
Accessing the Struct Fields
We can use the . annotation to access the fields of the Struct.
// Accessing the values
println!("The age of John Doe is {}.", john_doe.age);
While Assigning a field of a struct we have to be careful of the Heap Allocated types as their ownership can change.
For example:
In this code, we are having the name
filed in the struct
Person
a heap allocated type. Now when assigning this to a new variable.
// The ownership of the name field would move out of the Struct field to the variable
let full_name = john_doe.name;
This is called a partial move.
Partial Move ( Ownership change for Heap Allocated Data)
We have the initial code like this.
struct Person {
name: String,
age: u32,
address: String,
}
fn main () {
let john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
let new_person: Person = Person {
name: String::from("Mary Susan"),..john_doe
};
}
When doing this be careful of the partial moves which can happen in these cases. The fields which are heap allocated the ownership will be changed, the ownership ll move out to the original struct instance to new struct instance.
struct Person {
name: String,
age: u32,
address: String,
}
fn main () {
let john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
let _new_person: Person = Person {
name: String::from("Mary Susan"),..john_doe
};
println!("address is {}",john_doe.address);
}
// ERROR
error[E0382]: borrow of moved value: `john_doe.address`
--> src/main.rs:17:26
|
14 | let _new_person: Person = Person {
| _____________________________-
15 | | name: String::from("Mary Susan"),..john_doe
16 | | };
| |_- value moved here
17 | println!("address is {}",john_doe.address);
| ^^^^^^^^^^^^^^^^ value borrowed here after move
|
Extending the Structs Functionality using impl
We can add functionalities to the Struct using implementation where we use the impl keyword and mention the struct name whose functionality has to be extended which follows the syntax given below.
struct Person {
name: String,
age: u32,
address: String,
}
impl Person {
// Prints a general info about the person
fn generate_card_info(&self) -> () {
println!(
"Name: {}\nAge: {}\nAddress: {}",self.name,self.age,self.address);
}
}
fn main () {
let john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
// This is similar to calling methods in other programming laguages
john_doe.generate_card_info();
}
// Output
Name: John Doe
Age: 30
Address: 123 Fake Street
In the above example the function generate_card_info has a self parameter. Self is a keyword which is the instance of the Person. Now the fields of the struct is available inside the generate_card_info.
There are three ways the ownership is used in the implementation.
Lets look at the same example but with new methods.
- When we just use the reference to read the data and do not modify we can use the
&self
which is the reference and does not transfer the ownership - When we want to modify the data we can use the
&mut self
which is a mutable reference, like in the methodchange_age
- When we want to return a new instance of person by taking the ownership from the caller of the method. Since the method needs ownership and return the ownership back, the parameter will be
self
and the return type would beSelf
struct Person {
name: String,
age: u32,
address: String,
}
impl Person {
// Prints a general info about the person
fn generate_card_info(&self) -> () {
println!(
"Name: {}\nAge: {}\nAddress: {}",self.name,self.age,self.address);
}
// Change the age of the Person
fn change_age(&mut self, new_age: u32) -> () {
self.age = new_age;
}
// Return a new instance
fn new_person(self) -> Self {
self
}
}
fn main () {
let mut john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
// This is similar to calling methods in other programming laguages
john_doe.generate_card_info();
john_doe.change_age(42);
let _jame_smith: Person = john_doe.new_person();
}
Associated Functions in Struct
These functions are connected to the type itself, but do not operate on instances of that type.
Associated Functions do not take self
parameter as the input.
Associated Functions are not called using the .
syntax.
struct Person {
name: String,
age: u32,
address: String,
}
impl Person {
// An associated Function
fn age_correction() -> u32 {
12
}
// Prints a general info about the person
fn generate_card_info(&self) -> () {
println!(
"Name: {}\nAge: {}\nAddress: {}",self.name,self.age,self.address);
}
// Change the age of the Person
fn change_age(&mut self, new_age: u32) -> () {
// using the assoicated function to get the age correction
self.age = new_age + Person::age_correction();
}
// Return a new instance
fn new_person(self) -> Self {
self
}
}
fn main () {
let mut john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
// This is similar to calling methods in other programming laguages
john_doe.generate_card_info();
john_doe.change_age(42);
let _jame_smith: Person = john_doe.new_person();
}
One common pattern used for associated function is using it as a constructor in the Struct.
Constructor Pattern using new method in struct
struct Person {
name: String,
age: u32,
address: String,
}
impl Person {
fn new(name: String, age: u32, address: String) -> Self {
// Self {
// name: name,
// age: age,
// address: address
// }
Self {
name,
age,
address,
}
}
// An associated Function
fn age_correction() -> u32 {
12
}
// Prints a general info about the person
fn generate_card_info(&self) -> () {
println!(
"Name: {}\nAge: {}\nAddress: {}",self.name,self.age,self.address);
}
// Change the age of the Person
fn change_age(&mut self, new_age: u32) -> () {
// using the assoicated function to get the age correction
self.age = new_age + Person::age_correction();
}
// Return a new instance
fn new_person(self) -> Self {
self
}
}
fn main () {
let mut john_doe: Person = Person {
name: String::from("John Doe"),
age: 30,
address: String::from("123 Fake Street"),
};
// This is similar to calling methods in other programming laguages
john_doe.generate_card_info();
john_doe.change_age(42);
let _jame_smith: Person = john_doe.new_person();
// we will use new associated function to instantiate a new person
let new_person: Person = Person::new(String::from("maria jose"),25,String::from("123 Fake Street"));
}
If you can compare line 46 to line 60. This constructor patterns made the whole syntax much more elegant.
Tuple Structs
We use a normal struct when you need named fields, field documentation, and the flexibility to modify the structure in the future.
On the other hand, tuple structs are suitable when you have a simple structure with a small number of fields, and you prefer the concise syntax and pattern matching capabilities.
You can have different types in the Tuple structs.
fn main () {
// Tuple Structs
struct coordinates(i32, i32);
let my_point: coordinates = coordinates(12, 34);
println!("{}", my_point.0);
println!("{}", my_point.1);
}
Difference Between Structs and Enums
- Structs are different from enums in that enums are used to define a finite set of related values that can be used in a more limited way.
- Structs, on the other hand, are used to group related data together into a custom data type. While enums can only have a finite set of values, structs can hold any kind of data that you want to group together.
In short, structs in Rust are a way to group related data together into a custom data type, while enums are a way to define a set of related values that can be used in a more limited way.