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
- scope: The region (often a block) where a variable lives.
- move: Transferring ownership from one variable to another, after which the original can no longer use the value.
- drop: The automatic cleanup Rust performs when a variable leaves scope.
- 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
Stringhold 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:
i32has a fixed size, so copying is cheap and simple.Stringpoints to heap memory, so it needs careful handling.- Rust therefore moves
Stringby default but lets simple scalars implementCopy.
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.
💬 댓글
이 글에 대한 의견을 남겨주세요