Structs
- A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group.
- It is used to just define the data attributes as we do in Object Oriented Programming Languages.
- There are three types of Structs:
- Structs with Named Fields
- Tuple Structs
- Unit Structs
Associated Functions and Methods
- Functions defined for structs using the
impl
keyowrd are called associated functions. - The associated functions which accepts
self
as it's first argument are called methods.
Structs with Named Fields
-
In structs, we name each piece of data, so it's clear what they mean. This name and data type pair are called fields.
-
Struct definition:
#![allow(unused)] fn main() { struct User { active: bool, // A Field username: String, email: String, sign_in_count: u64, } }
-
Creating a struct's instance:
#![allow(unused)] fn main() { // If you specify mut, all the values will be mutable otherwise none let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; }
-
Taking out and updating the values:
#![allow(unused)] fn main() { user1.email = String::from("anotheremail@example.com"); }
-
Defining functions for structs
#![allow(unused)] fn main() { fn build_user(email: String, username: String) -> User { User { email, //We can write like this aslo-> email: email username, active: true, sign_in_count: 1, } } }
-
The struct update syntax (
..
), or spread operator in JS:#![allow(unused)] fn main() { // Initially let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; // After using the struct update syntax (..) let user2 = User { email: String::from("another@example.com"), ..user1 }; }
Note: This update syntax, works same as assignment operator
=
, so stack values will get copied and heap values will be moved. Since, username is a String, it's value will be moved fromuser1
touser2
, henceuser1
can't be used again. -
To prevent this problem of ownership transfer, we can use
&str
instead ofString
but when we use references in structs, it won't actually compile but will ask for lifetimes.// FAIL: Lifetime specifier not provided. struct User { username: &str, email: &str, sign_in_count: u64, active: bool, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; }
-
In this situation the compiler situation looks something like this:
--> src/main.rs:2:15 | 2 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 | struct User<'a> { 2 | username: &'a str, |
Tuple Structs
-
Using Tuple Structs without Named Fields to Create Different Types:
#![allow(unused)] fn main() { struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
-
To access their types, we use the
.
operator followed by the number of this argumnet.#![allow(unused)] fn main() { let color = Color(10, 25, 16); let red = color.0; let green = color.1; let blue = color.2; }
Unit Structs
-
They are structs without Any Fields (they act like
()
). -
They are Useful when we want to implement a trait on some type but don’t have any data that you want to store in the type itself.
#![allow(unused)] fn main() { struct AlwaysEqual; let subject = AlwaysEqual; }
Why do we use Structs?
-
It is a more sensible design choice to pass as minimum arguments as possible inside a function. For Example, if we need to calculate the area of rectangle, instead of passing
height
andwidth
, it would be cleaner to pass the whole rectangle. -
Now, this can be done with the tuples too. For Example,
let rect1 = (50, 30);
but the problem with this syntax is that any developer can confuse which one is width or height. -
To make this process clearer and cleaner, we use
struct
, so that we can combine the data and still keep the meaning of each attribute intact.struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 50, height: 30 }; println!("The area of the rectangle is {} square pixels", area(&rect1)); } // Passing Rectangle as a reference is important so that main fn // can retain it's ownership after this function is called. fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
Printing Variables
-
Ways to Print the variables:
{}
- Used to print variables with Display trait, for simple data types like int, string etc. we don't need to derive this attribute.{:?}
- Used to print complex variables with Debug trait, preferred for complex data type like struct, and we need to derive theDebug
attribute.{:#?}
- Works similarly like{:?}
, except it's preferred for structs with large number of fields.dbg!()
- It is a macro used with Debug trait to print the variables, file and line number. It prints tostderr
instead ofstdout
(whichprintln!()
uses). It takes ownership, so prefer sending references to it.
-
Here's an example of using the
dbg!()
macro:#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), // It'll resolve the expression `30 * scale`, as if dbg!() call was never there, it happens due to ownership transfer height: 50, }; dbg!(&rect1); // To maintian the scope of rect1 in main() we sent only the reference. }
-
The output looks like this:
Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/rectangles` [src/main.rs:10] 30 * scale = 60 [src/main.rs:14] &rect1 = Rectangle { width: 60, height: 50, }
-
You can read more about Derivable Traits and Attributes.
Structs with Method Syntax
-
When functions are defined in the context of a struct, enum or trait they are called as Methods.
-
The first parameter of a method is always
self
, which represents the instance.#[derive(Debug)] struct Rectangle { width: u32, height: u32, } // Everything inside the impl block is associated with Rectangle impl Rectangle { // &self is a short hand for self: `&self` (references are used to prevent mutation) // You can pass the following too: // self - Ownership of instance // &self - Reference to the instance {Currently Using} // &mut self - Mutable Reference to the instance fn area(&self) -> u32 { self.width * self.height } // It is possible to name methods same as fields of struct // Usually these methods are used as getters, to keep the fields private but provide read only accees using the methods fn width(&self) -> bool { self.width > 0 } // This is how we pass anotherr instance of same struct to a method fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 15, height: 25, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); // If we use rect1.width() - Rust unserstands it as method and // if we use rect1.width - Rust unserstands it as a field if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); }; // This is how we can pass second instance while calling a method on first instance println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); }
Note: When you call a method with object.something()
, Rust automatically adds in &
, &mut
, or *
so object matches the signature of the method. In other words, the following are the same:
#![allow(unused)] fn main() { p1.distance(&p2); (&p1).distance(&p2); }
-
It is possible to use different
impl
blocks, it is a valid syntax.#![allow(unused)] fn main() { impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
Associated Functions
-
All the functions defined under
impl
are associated functions. -
Methods are associated functions which has
self
as an argument and we use.
operator to access it. -
It is possible to define associated functions without passing
self
as the first argument, these functions are accessed through::
operator. -
Here's an example:
#![allow(unused)] fn main() { // Calling a method, also an associated function instance.method(some_argument); // Calling an associated function, without self as the first argument, hence not a method String::from("Hello, World!"); }
-
These associated functions are commonly used as constructors. Also, for the previous example of Rectangle, we can use it as follows:
#![allow(unused)] fn main() { impl Rectangle { // With this associated function we can create a new instance of Rectangle // by passing one value instead of two, hence creating a square. fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } } } }
-
It can be called like this:
#![allow(unused)] fn main() { let sq = Rectangle::square(3); }