Rust 04. Ownership, Borrowing, and Lifetimes
Summary
When learning Rust, the words you keep seeing are ownership, borrowing, and lifetime. Those ideas are the main reason Rust can provide memory safety without a garbage collector.
This post uses small String-based examples to explain how ownership moves, why borrowing exists, and when lifetime annotations appear. The practical model is simple: ownership defines responsibility for a value, references let you borrow without taking that responsibility, and lifetimes describe how borrowed references are related.
Document Information
- Written on: 2026-04-10
- Verification date: 2026-04-15
- Document type: tutorial
- Test environment: Cargo project,
Stringand reference examples,src/main.rs - Test version: rustc 1.94.0, cargo 1.94.0
- Source quality: only official documentation is used.
- Note: lifetime annotation examples are shown to explain the concept. In real code, the compiler often infers more than beginners expect.
Problem Definition
For beginners, ownership-related topics feel difficult for a few recurring reasons.
- the same assignment looks like a copy in some cases and a move in others
- it is easy to lose track of whether a function takes ownership or just borrows
- mutable and immutable borrow conflicts feel surprising at first
- lifetime syntax looks like a time concept, even though it is mainly a way to describe reference relationships
This post reduces that confusion by connecting scope, move, clone/copy, borrowing, dangling references, and lifetime annotations into one flow. It does not cover smart pointers, interior mutability, or advanced lifetime patterns.
How to read this post: keep asking who is responsible for a value and whether the code is only borrowing it temporarily. Do not try to memorize lifetime syntax first. Instead, watch why a moved value cannot be used through its old name and why a reference must not outlive the value it points to.
Verified Facts
- The core ownership rules are that each value has one owner, and the value is dropped when that owner goes out of scope. Evidence: What Is Ownership? Meaning: Rust assigns one cleanup responsibility for each value. That rule helps prevent double frees and references to values that no longer exist.
- Owned types such as
Stringmove on assignment,clone()creates a real duplicate, andCopytypes duplicate on assignment instead of moving. Evidence: What Is Ownership? Meaning: the same=can have different costs and meanings.Stringtransfers responsibility, while smallCopyvalues such asi32remain usable through the original binding. - Borrowing lets you use references without transferring ownership, and mutable borrowing follows stricter rules than immutable borrowing. Evidence: References and Borrowing Meaning: many readers can look at the same value, but a writer needs exclusive access. That is the core reason mutable borrowing is more restricted.
- Dangling references are rejected, and explicit lifetime annotations are sometimes required to describe how returned references relate to input references. Evidence: References and Borrowing, Validating References with Lifetimes Meaning: lifetimes are not runtime clocks. They are type-level descriptions of valid reference relationships.
- The beginner practice flow is easiest to follow inside a
cargo newproject. Evidence: Hello, Cargo! Meaning: ownership examples are most useful when you also see the compiler errors, so replacing code and rerunningcargo runis part of the learning loop.
Directly Confirmed Results
1. One small Cargo project made the examples easiest to rerun
- Direct result: the following setup worked well as a baseline for replacing
src/main.rsand rechecking each ownership example.
cargo new rust-ownership-basics
cd rust-ownership-basics
code .
cargo run
2. Scope explained the first ownership rule clearly
- Direct result: in the example below,
messagewas only valid inside the block, which made the owner-scope relationship easy to see.
fn main() {
{
let message = String::from("hello");
println!("{}", message);
}
// message is no longer valid here.
}
- How to read this: a block creates the range in which a value is valid. This was the simplest starting point for explaining why Rust ties values to owner scope.
3. String assignment behaved like a move, not a copy
- Direct result: assigning
s1tos2made the moved-ownership model much easier to understand.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s2);
}
- Direct result: trying to use
s1again produced a moved-value compile error.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);
println!("{}", s2);
}
error[E0382]: borrow of moved value: `s1`
-
How to read this: this does not mean the string data disappeared. It means responsibility for the value moved from
s1tos2. If both names need usable values, choose an explicit copy such asclone(). -
Direct result: using
clone()created a real duplicate.
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}", s1);
println!("s2 = {}", s2);
}
s1 = hello
s2 = hello
- Direct result:
Copytypes such asi32kept the original binding usable after assignment.
fn main() {
let x = 10;
let y = x;
println!("x = {}", x);
println!("y = {}", y);
}
x = 10
y = 10
- How to read this:
clone()creates separate string data, so boths1ands2remain usable.i32isCopy, so assignment keepsxusable as well. A useful beginner model is: owned heap-backed values often move, smallCopyvalues copy.
4. Borrowing kept ownership in place while still allowing access
- Direct result: passing a string by reference let the function read from it without taking ownership away from
main.
fn print_length(text: &str) {
println!("length = {}", text.len());
}
fn main() {
let message = String::from("hello rust");
print_length(&message);
println!("message = {}", message);
}
- Observed result:
length = 10
message = hello rust
-
How to read this:
print_length(&message)lends read access instead of transferring the value. After the function call,mainis still the owner ofmessage. -
Direct result: immutable borrows could coexist, but mutable borrows were only accepted when the code had exclusive access.
fn add_suffix(text: &mut String) {
text.push_str(" ownership");
}
fn main() {
let mut message = String::from("rust");
add_suffix(&mut message);
println!("{}", message);
}
rust ownership
- Direct result: mixing an immutable borrow and a mutable borrow at the same time produced the expected borrow-checker error.
fn main() {
let mut text = String::from("hello");
let r1 = &text;
let r2 = &mut text;
println!("{}, {}", r1, r2);
}
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable
- How to read this: the problem is not that mutable borrowing is bad. The problem is that the code asks for write access while a read reference is still in use.
5. Dangling references were blocked, and lifetimes described relationships
- Direct result: returning a reference to a local
Stringwas rejected.
fn dangle() -> &String {
let text = String::from("hello");
&text
}
- Direct result: returning ownership instead of a reference fixed that pattern.
fn no_dangle() -> String {
let text = String::from("hello");
text
}
- Direct result: the classic “return one of two borrowed strings” example showed where lifetime annotations become useful.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() {
x
} else {
y
}
}
fn main() {
let first = String::from("rust");
let second = String::from("ownership");
let result = longest(first.as_str(), second.as_str());
println!("longer = {}", result);
}
longer = ownership
- How to read this:
'ainlongest<'a>says that the two inputs and the returned reference are tied together by a valid reference relationship. Because the function may return eitherxory, the compiler needs that relationship stated.
6. Structs that stored references also needed lifetimes
- Direct result: when a struct field stored a reference, adding a lifetime parameter made the relationship explicit.
struct Highlight<'a> {
part: &'a str,
}
fn main() {
let article = String::from("Rust ownership makes memory safety practical.");
let first_word = article.split_whitespace().next().unwrap();
let highlight = Highlight { part: first_word };
println!("{}", highlight.part);
}
Rust
- How to read this: if a struct stores a reference, the struct must not outlive the original value behind that reference. Once immutable borrowing, mutable borrowing, lifetime annotations, and a reference-storing struct were placed side by side, the overall model became much easier to connect.
Interpretation / Opinion
- Key decision at this stage: ownership is easier to learn as “who is responsible for this value?” than as a purely memory-theory concept.
- Decision rule: if the old name is no longer needed, move the value; if both sides need their own value, use
clone; if only reading is needed, use&T; if mutation is needed, use&mut T. - Interpretation: lifetimes are less about measuring time and more about describing valid reference ranges to the compiler.
Limits and Exceptions
- This post is an introduction centered on
Stringand string references. It does not coverVec<T>, smart pointers, interior mutability, trait objects, or async borrowing issues. - Exact diagnostics can vary across Rust versions.
- Lifetime annotations are often omitted in real code when inference is enough, but this post keeps the classic examples visible for clarity.
- The focus is on the language rules themselves rather than OS-specific differences.
- Remaining questions after this post include smart pointers, interior mutability, trait objects, and async borrowing. Those are next-layer topics built on top of the same ownership rules.
댓글남기기