Implementing Method

1. Welcome and Icebreaker:

Welcome back!

If you could add a custom method to a real-world object (like a car, a phone, or a refrigerator), what would the method do?

2. Outline the Session:

Here’s the plan:

  1. Concept Explanation: We’ll review the syntax of impl blocks, the different types of methods (&self, &mut self, self, and associated functions), and how to chain method calls.
  2. Live Coding Demonstration: We’ll write code examples that demonstrate various method implementations, including methods that modify the struct, methods that return values, and associated functions.
  3. Tasks: You’ll get exercises that focus specifically on writing and using different types of methods within impl blocks.

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

  • Confidently use impl blocks to define methods for structs.
  • Write methods that take &self, &mut self, and self.
  • Write associated functions (including constructors).
  • Chain method calls together.
  • Understand the implications of each type of method (borrowing, mutability, ownership).

Expectations: This session builds directly on the previous one. Make sure you’re comfortable with the basic concepts of structs before proceeding.

3. Concept Explanation:

impl Block Syntax:

struct MyStruct {
    // Fields...
}

impl MyStruct { // Start of the implementation block for MyStruct
    // Methods and associated functions go here
}

Method Types (Review):

  • &self (Immutable Borrow): The method can access the struct’s data but cannot modify it.

    impl MyStruct {
        fn get_value(&self) -> i32 {
            self.value // Accessing a field
        }
    }
    
  • &mut self (Mutable Borrow): The method can both access and modify the struct’s data.

    impl MyStruct {
        fn set_value(&mut self, new_value: i32) {
            self.value = new_value; // Modifying a field
        }
    }
    
  • self (Ownership): The method takes ownership of the struct instance. The instance is moved into the method and is no longer available after the call. Useful for transformations where you want to consume the original value and produce a new one.

    impl MyStruct {
        fn transform(self) -> AnotherStruct {
            // ... create and return AnotherStruct based on self
            AnotherStruct{}
        }
    }
    
  • Associated Functions (No self): Functions associated with the struct type itself, not a specific instance. Often used for constructors.

    impl MyStruct {
        fn new(value: i32) -> MyStruct {
            MyStruct { value }
        }
    }
    

Method Chaining:

If a method returns self (or &self / &mut self), you can chain method calls together:

my_struct.method1().method2().method3();

This is possible because each method call returns a reference to the struct instance, allowing the next method to be called on it.

4. Live Coding Demonstration:

struct Counter {
    count: i32,
}

impl Counter {
    // Constructor (associated function)
    fn new() -> Counter {
        Counter { count: 0 }
    }

    // Immutable borrow of self
    fn get_count(&self) -> i32 {
        self.count
    }

    // Mutable borrow of self
    fn increment(&mut self) {
        self.count += 1;
    }

    // Take ownership of self
    fn reset(self) -> Counter {
        Counter { count: 0 } // Return a new Counter instance
    }
    // Method Chaining (mutable borrow of self)
    fn increment_by(&mut self, amount:i32) -> &mut Self{
        self.count += amount;
        self // Return a mutable reference to self
    }
}

struct Point {
    x: f64,
    y: f64,
}
// Multiple impl blocks (for organization)
impl Point {
    fn new(x:f64, y:f64) -> Point {
        Point {x, y}
    }
}

impl Point {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let mut counter = Counter::new();
    println!("Initial count: {}", counter.get_count()); // 0

    counter.increment();
    println!("Count after increment: {}", counter.get_count()); // 1

    let reset_counter = counter.reset(); // counter is moved here
    println!("Reset count: {}", reset_counter.get_count()); // 0
    //println!("Count after reset: {}", counter.get_count()); //Error: counter moved

    let mut counter2 = Counter::new();
    counter2.increment_by(5).increment_by(5); // Method Chaining
    println!("Count after chained increment: {}", counter2.get_count()); // 10

    let point = Point::new(3.0, 4.0);
    println!("Distance: {}", point.distance_from_origin());

}

Key Takeaways:

  • impl Block Usage: We define methods for Counter and Point within impl blocks.
  • Method Types: We demonstrate &self, &mut self, self, and associated functions.
  • Method Chaining: The increment_by method returns &mut Self, enabling chained calls.
  • Multiple impl blocks: We demonstrate multiple impl blocks.

5. Tasks

  1. Circle Methods: Create a Circle struct with a radius field (f64). Implement the following methods:
    • new(radius: f64) -> Circle: A constructor.
    • area(&self) -> f64: Calculates and returns the area.
    • grow(&mut self, increment: f64): Increases the radius by the given increment.
    • shrink(self, decrement: f64) -> Option<Circle>: Decreases the radius by the given decrement. If the resulting radius would be negative, return None. Otherwise, consume the original Circle and return a Some containing the new Circle.
  2. StringWrapper Methods: Create a struct called StringWrapper that holds a single String field. Implement:
    • new(s: String) -> StringWrapper: A constructor.
    • get_length(&self) -> usize: Returns the length of the string.
    • append(&mut self, other: &str): Appends a string slice to the internal string.
    • to_uppercase(self) -> StringWrapper: Consumes the StringWrapper, converts the internal string to uppercase, and returns a new StringWrapper containing the uppercase string.
    • trim_whitespace(&mut self) -> &mut Self: trim the internal string and return mutable self.
  3. TodoList Methods (Challenge): Create a TodoList struct that holds a Vec<String> representing a list of tasks. Implement:
    • new() -> TodoList: A constructor that creates an empty TodoList.
    • add_task(&mut self, task: String): Adds a task to the list.
    • complete_task(&mut self, index: usize) -> Option<()>: Marks a task as complete (e.g., by removing it from the list). Return None if the index is out of bounds.
    • get_tasks(&self) -> &[String]: Returns a slice of the tasks (an immutable borrow).
    • clear(self) -> TodoList: clear all tasks and return new TodoList.
  4. Chaining: Implement method chaining for Circle struct. Create some methods that return &mut Self.