Structs

  • Used to encapsulate related properties into one unified data type.
  • By convention, the name should follow PascalCase.
  • 3 variants,
    • C-like structs: One or more , separated name: value pairs enclosed in {}

      struct Color {
          red: u8,
          green: u8,
          blue: u8,
      }
      
    • Tuple structs: One or more , separated values enclosed in ()

      struct Color(u8, u8, u8);
      
    • Unit structs: A struct with no fields/ members

      struct Black;
      

⭐️ In Rust, data (attributes) and behavior (associated functions and methods) are placed separately. Structs and Enums are used to group related data, and impls and traits are used to add associated and shared behavior to that data.

πŸ’‘ In Rust, the term “instantiation” is used to describe the act of creating a concrete instance of a type (struct or enum).

πŸ’‘ In Rust, the term “field” is used to describe a named component in a C-like struct & struct-like enum variant, and the term “element” is used to describe an unnamed component in a tuple struct & tuple-like enum variant. The term “member” is used to describe both.

πŸ’― More complex examples can be found on Impls and Traits, Lifetimes and Modules sections.

C-like Structs

  • Similar to classes (without its methods) in OOP languages.
  • Can access fields using the ./ dot notation and the field name.

Definition

struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

Instantiation & Accessing Fields

struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

fn main() {
    // 1. Instantiation
    let white = Color {
        red: 255,
        green: 255,
        blue: 255,
    };

    // 2. Instantiation without redundant field names, when using the same variable names
    let (red, green, blue) = (0, 0, 0);
    let black = Color { red, green, blue };

    // 3. Instantiation + copy fields' values from another instance
    let red = Color { red: 255, .. black }; // πŸ’‘ Copy green and blue from black
    let green = Color { green: 255, .. black }; // πŸ’‘ Copy red and blue from black
    let mut blue = Color { .. black }; // πŸ’‘ Copy all fields' values from black
    blue.blue = 255;

     println!("RGB({}, {}, {})", white.red, white.green, white.blue); // RGB(255, 255, 255)
     println!("RGB({}, {}, {})", black.red, black.green, black.blue); // RGB(0, 0, 0)

     println!("RGB({}, {}, {})", red.red, red.green, red.blue); // RGB(255, 0, 0)
     println!("RGB({}, {}, {})", green.red, green.green, green.blue); // RGB(0, 255, 0)
     println!("RGB({}, {}, {})", blue.red, blue.green, blue.blue); // RGB(0, 0, 255)
}
// 4. Instantiation with default values

#[derive(Default)]
struct Person {
    name: String,
    age: f32,
}

fn main() {
    let a = Person::default(); // Instantiation with default values

    assert_eq!(a.name, ""); // String default value ""
    assert_eq!(a.age, 0.0); // f32 default value 0.0
}

πŸ’‘ In Rust, the #[derive()] attribute is used to automatically generate an implementation of certain traits for a custom data structure (struct and enum), instead of you writing them by hand. The std::default::Default trait allows us to create a new instance of a type with the Type::default() method.

πŸ’― 5. We can also use a constructor function inside an impl block to initialize a struct.

Destructuring

struct Person {
    name: String,
    company_name: String,
}

fn get_steve() -> Person {
    Person {
        name: "Steve Jobs".to_string(),
        company_name: "Apple".to_string(),
    }
}

fn main() {
    let steve = Person {
        name: "Steve Jobs".to_string(),
        company_name: "Apple".to_string(),
    };

    let Person {name: a, company_name: b} = steve; // 1. Destructuring fields' values to a and b
    println!("{a} {b}"); // Steve Jobs Apple

    let Person {company_name: c, .. } = get_steve(); // 2. Destructuring only selected fields' values; directly from the function call
    println!("{c}"); // Apple
}

// πŸ’― let Person {name: ref a, company_name: ref b} = steve; // add ref keyword, to pass a field's value as a reference

Tuple Structs

  • Looks like a named tuples.
  • Can access fields using the ./ dot notation and the index number of the field, like on tuples.
  • ⭐️ When a tuple struct has only one element, we call it newtype pattern. Because it helps to create a new type.

Definition

struct Color(u8, u8, u8);

struct Department(String);

Instantiation & Accessing Elements

struct Color(u8, u8, u8);

struct Department(String);

fn main() {
    let white = Color(255, 255, 255);
    println!("RGB({}, {}, {})", white.0, white.1, white.2); // RGB(255, 255, 255)

    let eng_department = Department("Engineering".to_string());
    println!("{}", eng_department.0); // Engineering
}

Destructuring

struct Color(u8, u8, u8);

struct Department(String);

fn get_department() -> Department {
    Department("Engineering".to_string())
}

fn main() {
    let white = Color(255, 255, 255);

    let Color(red, green, blue) = white; // πŸ’‘ let Color(red, blue, .. ) = white; // Destructuring only selected field's value
    println!("RGB({}, {}, {})", red, green, blue); // RGB(255, 255, 255)

    let Department(name) = get_department();
    println!("{}", name); // Engineering
}

Unit Structs

  • It defines a new type, but it resembles an empty tuple, ()
  • This is rarely useful on its own. But in combination with other features (such as generics), it can become useful.

Definition & Instantiation

struct Electron;

fn main() {
    let x = Electron;
}

πŸ“– ex: A library may ask you to create a structure that implements a certain trait to handle events. If you don’t have any data you need to store in the structure, you can create a unit-like struct.

Debug Printing and Pretty Debug Printing

In Rust, the #[derive()] attribute is used to automatically generate an implementation of certain traits for a custom data structure (struct and enum), instead of you writing them by hand. The std::fmt::Debug trait allows us to format a value with {:?} or {:#?} in println! and similar macros.

#![allow(unused)] // πŸ’‘ skip unused warnings, as we don't read fields in the structs

#[derive(Debug)]
struct Electron;

#[derive(Debug)]
struct Department(String);

#[derive(Debug)]
struct Person {
    name: String,
    company_name: String,
}

fn main() {
    let a = Electron;
    println!("{a:?}"); // Electron // πŸ’‘{a:#?} prints the same

    let b = Department("Engineering".to_string());
    println!("{b:?}"); // Department("Engineering")
    println!("{b:#?}");
    // Department(
    //     "Engineering",
    // )
    
    let c = Person { name: "Steve Jobs".to_string(), company_name: "Apple".to_string() };
    println!("{c:?}"); // Person { name: "Steve Jobs", company_name: "Apple" }
    println!("{c:#?}");
    // Person {
    //     name: "Steve Jobs",
    //     company_name: "Apple",
    // }
}