[Rust Series 8] Structs and Methods to Group Data

한국어 버전

Now that you can borrow strings and slices safely, it’s time to group several values under one type. Rust’s struct lets you name related pieces of data, and an [[impl|impl block]] defines the behavior that belongs to that data. In this part we’ll craft a small study log struct and confirm that immutable vs mutable reference rules apply to methods exactly the same way.

Defining a Struct

struct StudyLog {
    topic: String,
    completed: bool,
    notes: Vec<String>,
}

fn main() {
    let mut log = StudyLog {
        topic: String::from("borrowing"),
        completed: false,
        notes: vec![String::from("Review immutable references")],
    };

    println!("{} in progress", log.topic);
    log.notes.push(String::from("Write a mutable reference example"));
    println!("Note count: {}", log.notes.len());
}

Struct fields are private by default. This example lives in one module so we can access fields directly, but production code usually exposes methods to read or mutate fields.

impl Blocks and Methods

When you define methods, the first parameter is one of self, &self, or &mut self. Each option mirrors ownership rules:

  • &self: borrow immutably, read only
  • &mut self: borrow mutably, exclusive write access
  • self: take ownership of the instance
impl StudyLog {
    fn new(topic: &str) -> Self {
        Self {
            topic: topic.to_string(),
            completed: false,
            notes: Vec::new(),
        }
    }

    fn add_note(&mut self, memo: &str) {
        self.notes.push(memo.to_string());
    }

    fn summary(&self) -> String {
        format!(
            "{} / done: {} / {} notes",
            self.topic,
            self.completed,
            self.notes.len(),
        )
    }

    fn finish(self) -> StudyLog {
        StudyLog {
            completed: true,
            ..self
        }
    }
}

fn main() {
    let mut log = StudyLog::new("strings");
    log.add_note("Separate String and &str");
    log.add_note("Safe slice ranges");
    println!("{}", log.summary());

    let finished = log.finish();
    // println!("{}", log.summary()); // compile error: log moved into finish
    println!("Done state: {}", finished.summary());
}
  • new returns an owned StudyLog.
  • add_note needs &mut self, so no other references can be active during the call.
  • summary only reads, so &self is enough.
  • finish consumes self, so the caller can’t use the old value afterward.

Reusing Slices and References Inside Methods

Bringing the string concepts into methods looks like this:

impl StudyLog {
    fn latest_note(&self) -> Option<&str> {
        self.notes.last().map(|note| note.as_str())
    }
}

fn main() {
    let mut log = StudyLog::new("structs");
    log.add_note("Initialize fields");
    log.add_note("Build an impl block");

    if let Some(text) = log.latest_note() {
        println!("Latest note: {}", text);
    }
}

Option<&str> lets the struct keep ownership of each note while lending slices to callers. Instead of cloning strings, you return "Some(&str) if it exists, None otherwise" and let the borrow checker enforce safety.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Try It Yourself

  • Implement mark_done(&mut self) to toggle completed, then observe what happens if you also hold an immutable reference while calling it.
  • Add a minutes field that tracks study time and write a method that computes the average.
  • Experiment with passing the struct by self, &self, and &mut self to see which one communicates intent most clearly.

Wrap-Up

Structs and methods give Rust code context. Each field declares ownership explicitly, and methods mirror the borrowing rules so you always know who owns what. Up next we’ll represent richer state with enums and pattern matching, and wire Option into our structs.

💬 댓글

이 글에 대한 의견을 남겨주세요