Structs in RUST

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:

  1. Named field Struct
  2. Tuple Struct
  3. 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.

  1. 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
  2. When we want to modify the data we can use the &mut self which is a mutable reference, like in the method change_age
  3. 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 be Self
				
					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

  1. 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.
  2. 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.

Share

Facebook
Twitter
Pinterest
LinkedIn