[Rust Series 5] Ownership Basics

한국어 버전

After Sessions 1–4, it is time to confront why Rust can feel strict. Ownership tracks who owns each value and who drops it when the scope ends; it is how the compiler enforces memory safety. Session 5 walks through move, scope, and drop with small examples and sets expectations for borrowing in Session 6.

New terms in this post

  1. scope: The region (often a block) where a variable lives.
  2. move: Transferring ownership from one variable to another, after which the original can no longer use the value.
  3. drop: The automatic cleanup Rust performs when a variable leaves scope.
  4. Copy trait: Marks lightweight types so they get bitwise-copied instead of moved.

Core ideas

  • Every value in Rust has exactly one owner; when that owner leaves scope, the value is dropped.
  • Types like String hold heap data and therefore move by default—assigning them transfers ownership.
  • Types that implement Copy (integers, booleans, characters, etc.) are duplicated instead of moved.
  • Passing or returning values across function boundaries also triggers ownership moves, so you must track lifetimes at those points.

Beginners usually ask, "Why is i32 fine but String isn't?" Keep this intuition for now:

  • i32 has a fixed size, so copying is cheap and simple.
  • String points to heap memory, so it needs careful handling.
  • Rust therefore moves String by default but lets simple scalars implement Copy.

Code along

1. Scope and drop

fn main() {
    {
        let name = String::from("Mathbong");
        println!("Hello, {name}");
    } // name is dropped here and its memory is freed

    // println!("{name}"); // compile error: out of scope
}

Once the block ends, name is invalid and Rust calls drop automatically.

2. Move vs. Copy

fn main() {
    let original = String::from("ownership");
    let moved = original; // move

    // println!("{original}"); // compile error
    println!("moved = {moved}");

    let x = 10;
    let y = x; // Copy (i32 implements Copy)
    println!("x = {x}, y = {y}");
}

String holds heap pointers, so ownership moves. i32 gets copied instead.

3. Ownership at function boundaries

fn main() {
    let title = String::from("Rust 101");
    takes_ownership(title);
    // println!("{title}"); // already moved

    let score = 95;
    makes_copy(score);
    println!("score still usable: {score}");

    let returned = gives_back();
    println!("Returned value: {returned}");
}

fn takes_ownership(text: String) {
    println!("Function received ownership: {text}");
} // text drops here

fn makes_copy(number: i32) {
    println!("Function received Copy: {number}");
} // number copies, so caller keeps using it

fn gives_back() -> String {
    let note = String::from("Ownership returned via return");
    note
}

Passing a String argument moves ownership into the function. To use the value afterward, you must either return it (as above) or borrow it (Session 6).

4. Workarounds to avoid moves

fn main() {
    let text = String::from("hello");
    let (len, text) = calculate_length(text);
    println!("{text} has length {len}");
}

fn calculate_length(input: String) -> (usize, String) {
    let length = input.len();
    (length, input)
}

You can return tuples that include both the result and the original data. It works, but borrowing with & is usually cleaner, which is why we learn it next.

Why it matters

  • Ownership is the reason Rust manages memory safely without a garbage collector.
  • Understanding move and scope lets you decode compiler errors quickly and figure out how to regain ownership.
  • Tracking ownership at function boundaries prevents mistakes once you learn borrowing, slices, structs, and collections.

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.

Practice

  1. Write code that tries to print a String twice and capture the compiler error explaining which ownership rule you broke.
  2. Implement fn takes_and_returns(value: String) -> String to practice taking ownership and giving it back.
  3. Compare a non-Copy type (Vec<i32>, String) with a Copy type (u8, bool) in a short program to see the difference.
  4. Place println! statements to show exactly when a block-created value is dropped as the scope ends.

Wrap-up

Ownership boils down to two rules: each value has a single owner, and it drops when the scope ends. If you experimented with moves and function calls here, borrowing and reference rules in Session 6 will make far more sense.

💬 댓글

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