Structs and Methods

1. Welcome and Icebreaker:

Welcome back!

If you could design a custom data structure (a struct) to represent a real-world object, what object would you choose, and what fields would it have?

2. Outline the Session:

Here’s the plan:

  1. Concept Explanation: We’ll define structs, their syntax, and different kinds of structs (named-field, tuple, and unit-like). Then, we’ll introduce methods, how they’re associated with structs, and the different types of methods (instance methods, associated functions).
  2. Live Coding Demonstration: We’ll write code to demonstrate how to define structs, create instances, access fields, and define and call methods.
  3. Tasks: You’ll get exercises to practice creating your own structs and methods.

Learning Outcomes: By the end of this session, you will be able to:

  • Define and use structs with named fields.
  • Define and use tuple structs.
  • Understand unit-like structs.
  • Define methods on structs using impl blocks.
  • Understand the self, &self, and &mut self parameters.
  • Differentiate between instance methods and associated functions.
  • Call methods on struct instances.

Expectations: Structs and methods are fundamental building blocks for object-oriented programming in Rust. Pay close attention to how methods are associated with structs and how the self parameter works.

3. Concept Explanation:

Structs (Structures):

  • Structs allow you to create custom data types that group together related data.
  • They are similar to classes in other languages, but without inheritance.
  • Types of Structs:
    • Named-field structs: Have named fields, like classes in other languages.

      struct Person {
          name: String,
          age: u32,
      }
      
    • Tuple structs: Fields are unnamed, like tuples.

      struct Point(i32, i32, i32);
      
    • Unit-like structs: Have no fields. They are useful as markers or when you need to implement a trait on a type but don’t need any data.

      struct Empty;
      

Methods:

  • Methods are functions associated with a particular type (in this case, a struct).
  • They are defined within an impl (implementation) block.
  • Types of Methods:
    • Instance Methods: Take self, &self, or &mut self as their first parameter. They operate on an instance of the struct.
      • self: Takes ownership of the struct instance. The instance is moved into the method and is no longer available after the method call.
      • &self: Borrows the struct instance immutably. This is the most common type of method.
      • &mut self: Borrows the struct instance mutably, allowing the method to modify the instance.
    • Associated Functions: Do not take self as their first parameter. They are associated with the struct itself, not a particular instance. They are often used as constructors (like new).

4. Live Coding Demonstration:

// Named-field struct
struct Rectangle {
    width: u32,
    height: u32,
}

// Tuple struct
struct Point(i32, i32);

// Unit-like struct
struct Unit;

// Implementation block for Rectangle
impl Rectangle {
    // Associated function (constructor)
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }

    // Instance method (immutable borrow of self)
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // Instance method (mutable borrow of self)
    fn resize(&mut self, new_width: u32, new_height: u32) {
        self.width = new_width;
        self.height = new_height;
    }
    // Instance method ( move self)
    fn to_square(self) -> Rectangle {
        let size = self.width.max(self.height); // pick longest side for new square
        Rectangle { width: size, height: size }
    }
}

impl Point {
    // Associated function to create a Point at the origin
    fn origin() -> Point {
        Point(0, 0)
    }

    // Instance method to calculate the distance to another Point
    fn distance(&self, other: &Point) -> f64 {
        let dx = (self.0 - other.0) as f64;
        let dy = (self.1 - other.1) as f64;
        (dx * dx + dy * dy).sqrt()
    }
}
//Methods can be defined in multiple impl blocks
impl Rectangle{
      fn perimeter(&self) -> u32{
        2*(self.width + self.height)
    }
}
fn main() {
    // Creating instances of structs
    let mut rect = Rectangle::new(10, 5); // Using the constructor
    let origin_point = Point::origin(); // Using associated function
    let point = Point(3, 4);
    let _unit = Unit; // Instance of a unit-like struct

    // Accessing fields
    println!("Rectangle width: {}, height: {}", rect.width, rect.height);
    println!("Point coordinates: ({}, {})", point.0, point.1);

    // Calling methods
    let area = rect.area();
    println!("Area: {}", area);

    rect.resize(20, 10); // Modifying the rectangle
    println!("Resized width: {}, height: {}", rect.width, rect.height);
    println!("Rectangle perimeter: {}", rect.perimeter());

    let distance = origin_point.distance(&point);
    println!("Distance between origin and point: {}", distance);

     let square = rect.to_square();
     println!("New Square width: {}, height: {}", square.width, square.height);
    //println!("Old Rect width: {}, height: {}", rect.width, rect.height); // Error moved
}

Key Takeaways:

  • Struct Definitions: We define different types of structs.
  • impl Blocks: Methods are defined within impl blocks, associating them with the struct.
  • self Parameters: We demonstrate &self (immutable borrow), &mut self (mutable borrow) and self (move).
  • Associated Functions: Rectangle::new and Point::origin are associated functions (constructors).
  • Instance Methods: area, resize, and distance are instance methods.
  • Field Access: We access struct fields using dot notation.
  • Multiple impl: We can define methods in multiple impl blocks.

5. Tasks

  1. Person Struct: Create a Person struct with fields for name (String), age (u32), and email (Option<String>). Create an associated function new to construct a Person. Create an instance method greet that takes &self and prints a greeting message including the person’s name and age. If the person has an email address, print the email address as well.

  2. Circle Struct: Create a Circle struct with a field for radius (f64). Implement the following:

    • An associated function new to create a Circle.
    • An instance method area that calculates the area of the circle.
    • An instance method circumference that calculates the circumference.
  3. BankAccount Struct: Create a BankAccount struct with fields for account_number (String) and balance (f64). Implement the following:

    • An associated function new that takes an account number and an initial balance.
    • An instance method deposit that takes &mut self and an amount, and adds the amount to the balance.
    • An instance method withdraw that takes &mut self and an amount. If the account has sufficient funds, subtract the amount from the balance and return Some(()). If there are insufficient funds, return None.
    • An instance method get_balance that takes &self and returns the current balance.
  4. Tuple Struct and Methods: Create a tuple struct called Color that represents an RGB color with three u8 values (red, green, blue). Implement an instance method brightness that calculates a simple brightness value (e.g., the average of the three components).

  5. Unit-like Struct and Trait (Challenge): Create a unit-like struct called Logger. Although we haven’t covered traits yet, try to implement a simple trait for it. Here’s the basic structure:

    trait Loggable {
        fn log(&self, message: &str);
    }
    
    struct Logger;
    
    impl Loggable for Logger {
        fn log(&self, message: &str) {
            println!("LOG: {}", message);
        }
    }
    
    fn main() {
        let logger = Logger;
        logger.log("This is a log message.");
    }
    

    This demonstrates how a unit-like struct can be useful for implementing behavior without holding any data.

  6. Multiple impl blocks: Create a struct and define some methods in one impl block and some methods in other impl blocks.