9.14 Exercises

Practice applying the concepts learned in this chapter.

Click to see the list of suggested exercises

Exercise 1: Basic Struct and Methods

Define a Circle struct with a radius field (type f64). Implement the following in an impl block:

  1. An associated function new(radius: f64) -> Circle to create a circle.
  2. A method area(&self) -> f64 to calculate the area (π * r^2). Use std::f64::consts::PI.
  3. A method grow(&mut self, factor: f64) that increases the radius by factor.

Instantiate a circle, calculate its area, grow it, and calculate the new area.

use std::f64::consts::PI;

struct Circle {
    radius: f64,
}

impl Circle {
    // Associated function (constructor)
    fn new(radius: f64) -> Self {
        Circle { radius }
    }

    // Method to calculate area
    fn area(&self) -> f64 {
        PI * self.radius * self.radius
    }

    // Method to grow the circle
    fn grow(&mut self, factor: f64) {
        self.radius += factor;
    }
}

fn main() {
    let mut c = Circle::new(5.0);
    println!("Initial Area: {}", c.area());
    c.grow(2.0);
    println!("Radius after growing: {}", c.radius);
    println!("New Area: {}", c.area());
}

Exercise 2: Tuple Struct and Newtype Pattern

Create a tuple struct Kilograms(f64) to represent weight. Implement the Add trait from std::ops::Add for it, so you can add two Kilograms values together. Demonstrate its usage.

use std::ops::Add;

#[derive(Debug)] // Add Debug for printing
struct Kilograms(f64);

// Implement the Add trait for Kilograms
impl Add for Kilograms {
    type Output = Self; // Result of adding two Kilograms is Kilograms

    fn add(self, other: Self) -> Self {
        Kilograms(self.0 + other.0) // Access inner f64 using .0
    }
}

fn main() {
    let weight1 = Kilograms(10.5);
    let weight2 = Kilograms(5.2);
    let total_weight = weight1 + weight2; // Uses the implemented Add trait
    println!("Total weight: {:?}", total_weight); // e.g., Total weight: Kilograms(15.7)
    println!("Value: {}", total_weight.0); // Access the inner value
}

Exercise 3: Struct with References and Lifetimes

Define a struct DataView<'a> that holds an immutable reference (&'a [u8]) to a slice of bytes. Implement a method len(&self) -> usize that returns the length of the slice. Demonstrate creating an instance and calling the method.

struct DataView<'a> {
    data: &'a [u8],
}

impl<'a> DataView<'a> {
    fn len(&self) -> usize {
        self.data.len()
    }
}

fn main() {
    let my_data: Vec<u8> = vec![10, 20, 30, 40, 50];
    // Create a view of part of the data (elements at index 1, 2, 3)
    let data_view = DataView { data: &my_data[1..4] };

    println!("Data slice: {:?}", data_view.data); // e.g., Data slice: [20, 30, 40]
    println!("Length of view: {}", data_view.len()); // e.g., Length of view: 3
}

Exercise 4: Generic Struct with Trait Bounds

Create a generic struct MinMax<T> that holds two values of type T. Implement a method get_min(&self) -> &T that returns a reference to the smaller of the two values. This method should only be available if T implements the PartialOrd trait. Demonstrate its usage with numbers and potentially strings.

use std::cmp::PartialOrd;

struct MinMax<T> {
    val1: T,
    val2: T,
}

impl<T: PartialOrd> MinMax<T> {
    // This method only exists if T can be partially ordered
    fn get_min(&self) -> &T {
        if self.val1 <= self.val2 {
            &self.val1
        } else {
            &self.val2
        }
    }
}

// We can still have methods that don't require PartialOrd
impl<T> MinMax<T> {
    fn new(v1: T, v2: T) -> Self {
        MinMax { val1: v1, val2: v2 }
    }
}

fn main() {
    let numbers = MinMax::new(15, 8);
    println!("Min number: {}", numbers.get_min()); // 8

    let strings = MinMax::new("zebra", "ant");
    println!("Min string: {}", strings.get_min()); // "ant"

    // struct Unorderable; // A struct that doesn't implement PartialOrd
    // let custom = MinMax::new(Unorderable, Unorderable);
    // // custom.get_min(); // Error! Unorderable does not implement PartialOrd
}

Exercise 5: Destructuring, Update Syntax, and Printing

Define a Config struct with fields host: String, port: u16, use_https: bool.

  1. Derive Debug and Default.
  2. Create a default Config instance and print it using debug format.
  3. Create a new Config instance, overriding only the host field using struct update syntax and the default instance. Print this instance too.
  4. Write a function print_host_only(&Config { ref host, .. }: &Config) that uses destructuring to print only the host. Call this function.
#[derive(Default, Debug)] // Derive Default and Debug
struct Config {
    host: String,
    port: u16,
    use_https: bool,
}

// Function using destructuring in parameter
fn print_host_only(&Config { ref host, .. }: &Config) { // Use 'ref' to borrow String
    println!("Host from function: {}", host);
}

fn main() {
    // 1. Create and print default config
    let default_config = Config::default();
    println!("Default config: {:?}", default_config);

    // 2. Create and print custom config using struct update syntax
    let custom_config = Config {
        host: String::from("api.example.com"),
        ..default_config // Use default values for port and use_https
    };
    println!("Custom config: {:?}", custom_config);

    // 3. Call function that destructures the parameter
    print_host_only(&custom_config); // Pass a reference
}