References & Borrowing
Borrowing Instead of Moving
Section titled “Borrowing Instead of Moving”Moving a value into a function is often unnecessary — you just want the function to read the value, not own it. Rust solves this with references. A reference lets you pass a value to a function without giving up ownership.
The syntax &T means “a shared reference to a value of type T”. Creating a reference is called borrowing.
fn calculate_length(s: &String) -> usize { s.len()} // s goes out of scope, but since it does not own the data, nothing is dropped
fn main() { let s = String::from("hello"); let len = calculate_length(&s); // pass a reference, not the value println!("{} has {} characters", s, len); // s is still valid}The &s syntax creates a reference to s. The function signature s: &String declares that the parameter is a reference, not an owned String.
References Do Not Own
Section titled “References Do Not Own”Because a reference does not own the value it points to, the value is not dropped when the reference goes out of scope. Ownership stays with the original variable.
fn print_greeting(name: &String) { println!("Hello, {}!", name);} // reference dropped here — name itself is unaffected
fn main() { let name = String::from("Alice"); print_greeting(&name); print_greeting(&name); // can borrow again — we still own name println!("Still have: {}", name);}Multiple Shared References
Section titled “Multiple Shared References”You can have any number of shared (&T) references to a value at the same time, as long as none of them are mutable references. Shared references are read-only.
fn main() { let s = String::from("shared"); let r1 = &s; let r2 = &s; let r3 = &s; println!("{} {} {}", r1, r2, r3); // all three are valid simultaneously}References and Function Signatures
Section titled “References and Function Signatures”When a function parameter is &String, the caller passes &my_string. When the parameter is &str (a string slice), the function is more flexible — it accepts both &String and string literals. Prefer &str over &String for function parameters as a general rule.
fn greet(name: &str) { println!("Hello, {}!", name);}
fn main() { let owned = String::from("Bob"); greet(&owned); // &String coerces to &str greet("Charlie"); // string literal is already &str}&str vs &String vs String: Which One Do I Write?
Section titled “&str vs &String vs String: Which One Do I Write?”Three text types show up in almost every Rust signature, and mixing them up is the single most common source of unnecessary .clone()s and .to_string()s in AI-generated code. The choice comes down to one question: does this function need to own the text afterward?
flowchart TD
Q{Does the function need to own the text afterward?}
Q -->|Yes - store it, move it into a struct, send it elsewhere| OWN[Parameter type: String]
Q -->|No - it only reads the text| REF[Parameter type: &str]
REF -->|Caller has a String| COERCE1[Pass &my_string - auto-coerces to &str]
REF -->|Caller has a literal| COERCE2[Pass the literal directly - it is already &str] | Situation | Use | Why |
|---|---|---|
| Function only reads the text | &str | Accepts both &String (via coercion) and string literals — the most flexible input type |
| Function must keep, store, or move the text elsewhere | String | Taking ownership is unavoidable if the value needs to outlive the call |
| Returning a view into data the caller already owns | &str tied to an input’s lifetime | No allocation; the returned slice borrows from the input |
| Returning brand-new text built inside the function | String | You cannot return a reference to data that lives only inside the function (see below) |
A function parameter written as &String | Almost never the right choice | It is strictly less flexible than &str for no benefit — always prefer &str |
Returning References and a Lifetime Teaser
Section titled “Returning References and a Lifetime Teaser”Taking a reference as a parameter is straightforward — the compiler already knows the reference cannot outlive its input. Returning a reference is where it gets interesting: the compiler needs to know which input the returned reference is allowed to borrow from, because different call sites might pass arguments with different lifespans.
// @expect-error E0106fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("world!"); println!("{}", longest(&s1, &s2));}error[E0106]: missing lifetime specifier --> src/main.rs:1:33 |1 | fn longest(a: &str, b: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`help: consider introducing a named lifetime parameter |1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { | ++++ ++ ++ ++The fix is exactly what the compiler suggests: a named lifetime parameter, 'a, that ties the return value’s validity to both inputs at once.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("world!"); println!("longest: {}", longest(&s1, &s2));}<'a> does not change how long anything lives — it is a promise, checked at compile time, that the returned &str will not outlive whichever of a or b is shorter-lived. This is only a teaser: the full set of elision rules, 'static, and lifetimes on structs get their own lesson in Lifetimes.
fn calculate_length(s: &String) -> usize { s.len()}
fn greet(name: &str) { println!("Hello, {}!", name);}
fn main() { let s = String::from("hello");
// Pass a reference — s is not moved let len = calculate_length(&s); println!("length of {} is {}", s, len);
// Multiple shared borrows are allowed let r1 = &s; let r2 = &s; println!("r1={} r2={}", r1, r2);
// &String coerces to &str let owned = String::from("Alice"); greet(&owned); // borrow, not move greet("Bob"); // string literal println!("owned is still: {}", owned);}Compiling…
Mental Model
Section titled “Mental Model”A reference is a promise, not a pointer with extra syntax: &T says “let me look at this value, but I will never outlive it.” The compiler tracks, for every reference, exactly which owned value it points to and refuses to compile any path where the reference could still be read after that owner is gone. Returning a reference just makes that promise explicit in the function signature — a lifetime parameter names whose promise the return value inherits.
Trade-offs
Section titled “Trade-offs”Borrowing avoids an allocation and a copy — &str/&String cost nothing beyond a pointer and a length. The price is that a borrowed value’s lifetime becomes part of your function’s public contract: sometimes the compiler cannot express what you want (a function that sometimes returns a reference and sometimes needs to construct new text, for instance), and the honest fix is to return an owned String instead of fighting the borrow checker with lifetime annotations. Knowing when to stop borrowing and start owning is itself a skill — the decision table above is the fast path for parameters; return types often need the same judgment call.
Common Misconceptions
Section titled “Common Misconceptions”- “A reference is basically a raw pointer, so
fn f() -> &Stringshould work like returning a pointer does in C.” In C, returning a pointer to a local is a silent bug (a dangling pointer, undefined behavior on use). In Rust it does not compile at all —E0106/E0515-style errors exist specifically to catch this at compile time, because the compiler has nowhere valid to say the reference is “borrowed from.” - “
&Stringand&strare interchangeable, so it doesn’t matter which one I write in a signature.” They coerce for the caller’s convenience, but as a parameter type&stris strictly more flexible — it accepts literals that&Stringcannot — with no downside. There is essentially never a reason to write&Stringin a signature. - “Since I can have many shared references at once, I should be able to write through one of them too.”
&Tis read-only no matter how many references exist — the exclusivity rule that lets you write through a reference (&mut T) is a completely separate, stricter guarantee, covered next in Mutable Borrows.
Reading AI-generated code
Section titled “Reading AI-generated code”AI assistants frequently write a function parameter as &Vec<T> (a reference to an owned, growable vector) when the function only ever iterates or indexes — &[T], a slice, does the same job while also accepting arrays and any partial slice of a vector.
🐛 Spot the bug
fn sum_all(v: &Vec<i32>) -> i32 { v.iter().sum()}
fn main() { let numbers = vec![1, 2, 3, 4]; println!("{}", sum_all(&numbers));}Show the answer
This compiles and runs fine — the bug is a flexibility smell, not a compile error. &Vec<i32> only accepts a reference to an actual Vec<i32>; it rejects an array ([i32; 4]) and any already-sliced portion of a vector (&numbers[1..3]) without an extra explicit re-slice. Changing the parameter to &[i32] accepts all of those automatically via coercion, with the exact same function body — the Slices lesson covers why &[T]/&str generalize &Vec<T>/&String this way.