← Back to Rust series
🦀
Ownership
Ownership · Prerequisite: lesson 06 · Ownership · Prerequisite: lesson 07

07–08. References and Borrowing · Slices — &str and &[T]

Borrowing lets you use a value without taking ownership — `&` for an immutable reference, `&mut` for a mutable one. The core rule is that at any moment you have either (N shared references) OR (1 exclusive mutable reference), never mixed. This is what blocks data races at compile time. A slice is a reference view into part of a collection. `&str` is a view into a String or into a static string literal; `&[T]` is a view into a Vec or array. Slices supercharge function signatures and explain why `&str` is the idiomatic argument type rather than `String`.

Rustborrowreference&T&mut Tborrow checkerslice&str&[T]String
Duration
⏱ ~1.5 hours + ~1 hour
Level
📊 Intermediate
Prerequisite
🎯 Lesson 06
OUTCOME
Borrowing lets you use a value without taking ownership — `&` for an immutable reference, `&mut` for a mutable one. The core rule is that at any moment you have either (N shared references) OR (1 exclusive mutable reference), never mixed. This is what blocks data races at compile time. / A slice is a reference view into part of a collection. `&str` is a view into a String or into a static string literal; `&[T]` is a view into a Vec or array. Slices supercharge function signatures and explain why `&str` is the idiomatic argument type rather than `String`.

What you'll learn

  • 1Create references with `&` and `&mut`, and dereference them
  • 2State the borrow rule (N shared or 1 exclusive)
  • 3Take references as function arguments to avoid move
  • 4Read and fix borrow-checker errors
  • 5Understand NLL (Non-Lexical Lifetimes) and reference scoping
  • 6Create and index `&str` and `&[T]` slices
  • 7Accept `&str` arguments to increase caller flexibility
  • 8Know slices are fat pointers (pointer + length)
  • 9Know string literals are `&'static str`
  • 10Identify where wrong slice ranges panic at runtime

07. References and Borrowing (&T, &mut T, the borrow rules)

Borrowing lets you use a value without taking ownership — `&` for an immutable reference, `&mut` for a mutable one. The core rule is that at any moment you have either (N shared references) OR (1 exclusive mutable reference), never mixed. This is what blocks data races at compile time.

Overview

After lesson 06's move, passing a value to a function means losing it. Returning to keep using it is ugly. Borrowing fixes this — and on the same axis it **prevents data races at compile time**.

Core Concepts

1) Two kinds of references

  • **&T** — shared (immutable) reference. Read-only. Multiple allowed
  • **&mut T** — exclusive (mutable) reference. Read-write. Only one at a time

2) The borrow rule (one-liner)

**At any moment for a given value, you have (N shared references) OR (1 exclusive reference) — never both.**

3) Why? — preventing data races

If one thread is reading while another writes, you have a race. Rust prevents this **even in single-threaded code**, and the same rule scales to multi-threaded contexts. One exclusive mutator at a time = no races.

4) NLL — Non-Lexical Lifetimes

A reference is valid up to its **last use**, not the end of the enclosing scope. This makes shared → exclusive → shared sequences just work naturally.

CombinationAllowed?
multiple &x✓
single &mut x✓
&x + &x✓
&x + &mut x✗
&mut x + &mut x✗

Hands-on Examples

Pass by reference — no move:

rust
fn print_len(s: &String) {
    println!("len={}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_len(&s);          // lent
    println!("{}", s);     // OK — s still alive
}

Mutate through &mut:

rust
fn add_world(s: &mut String) {
    s.push_str(" world");
}

fn main() {
    let mut s = String::from("hello");
    add_world(&mut s);
    println!("{}", s); // hello world
}

Two simultaneous mutable borrows are a compile error:

rust
let mut x = 5;
let r1 = &mut x;
let r2 = &mut x; // error[E0499]: cannot borrow `x` as mutable more than once
println!("{} {}", r1, r2);

NLL — after last use, a new exclusive borrow is fine:

rust
let mut x = 5;
let r1 = &x;
let r2 = &x;
println!("{} {}", r1, r2);  // r1, r2 last used here
let r3 = &mut x;             // OK — the shared ones are done
*r3 += 1;
println!("{}", r3);

Common Mistakes

Q. Do I have to write `&mut` on both sides of the call?

A. Yes — the caller writes `add_world(&mut s)` explicitly. Reading the call site tells you immediately where mutation happens.

Q. Do references auto-deref for method calls?

A. Yes — `s.len()` works on a reference because the compiler inserts deref automatically. For raw dereference use `*r`.

Q. The borrow checker feels too strict. What now?

A. Read the error message — Rust's compiler nearly always suggests a fix. Usually a small restructuring (tightening scopes, avoiding shared+exclusive at the same time) unblocks you.

Recap

  • Borrow = lend without transferring ownership: `&`, `&mut`
  • (N shared) OR (1 exclusive) at any time — never mixed
  • Data-race prevention is enforced even single-threaded
  • NLL lets you re-borrow after the previous reference's last use

Try It Yourself

  1. Write a length-printing function in both reference and owning forms; observe the caller-side difference
  2. Write `fn add_one(v: &mut Vec<i32>)` that adds 1 to every element
  3. Create two `&mut` to the same variable on purpose to reproduce the error, then copy the message

08. Slices — &str and &[T]

A slice is a reference view into part of a collection. `&str` is a view into a String or into a static string literal; `&[T]` is a view into a Vec or array. Slices supercharge function signatures and explain why `&str` is the idiomatic argument type rather than `String`.

Overview

A function that takes `String` forces the caller to allocate. Worse, you can't pass a string literal `"hello"` directly. Slices solve this — accept `&str` and both Strings and literals work.

Core Concepts

1) Slice = reference view

A slice is a fat pointer carrying both **data pointer + length** (16 bytes: pointer 8 + length 8). It does not own data, it borrows.

2) &str — string slice

  • A string literal `"hello"` has type `&'static str` — alive for the program's entire lifetime
  • Sub-slice a String with `&s[0..5]`
  • Slicing at a non-**UTF-8 boundary** panics at runtime

3) &[T] — array / Vec slice

  • `&v[1..4]` for a partial slice
  • A function `fn sum(v: &[i32])` accepts arrays and Vecs alike

4) Function signature idiom

When designing a library function, prefer **`&str` over `String`, `&[T]` over `Vec<T>`** as arguments. Callers can pass more shapes, and unnecessary clones disappear.

Hands-on Examples

A function that accepts both Strings and literals — via &str:

rust
fn shout(s: &str) {
    println!("{}!", s.to_uppercase());
}

fn main() {
    let owned = String::from("hello");
    shout(&owned);   // String → &str via Deref
    shout("world"); // literal passes directly
}

Building and indexing slices:

rust
fn main() {
    let s = String::from("hello world");
    let hello: &str = &s[0..5];
    let world: &str = &s[6..11];
    println!("{} / {}", hello, world);

    let arr = [10, 20, 30, 40, 50];
    let mid: &[i32] = &arr[1..4]; // [20, 30, 40]
    println!("{:?}", mid);
}

A generic sum over slices:

rust
fn sum(v: &[i32]) -> i32 {
    let mut s = 0;
    for x in v { s += x; }
    s
}

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    println!("{}", sum(&v));       // Vec → &[i32]
    println!("{}", sum(&[10, 20])); // array → &[i32]
    println!("{}", sum(&v[1..4]));   // slice
}

Common Mistakes

Q. Slicing a Korean string panics

A. In UTF-8 a Korean character is 3 bytes. `&"안녕"[0..1]` slices in the middle of a character → panic. For per-character iteration use `.chars()`.

Q. Can I take a `String` argument?

A. You can, but `&str` is more general. Take `String` only when your function genuinely needs to own the data (e.g., to store it).

Q. Is the length known at compile time?

A. No — slice length is runtime, which is why slices carry the length inline (fat pointer). They're twice the size of a thin reference.

Recap

  • A slice is a (pointer, length) reference view
  • &str / &[T] for strings / arbitrary types
  • Take &str·&[T] in function signatures to accept more callers naturally
  • Slicing at a non-UTF-8 boundary panics at runtime

Try It Yourself

  1. Write `fn first_word(s: &str) -> &str` returning the first space-separated word
  2. Find a Vec<i32>'s max via `fn max(v: &[i32]) -> i32`
  3. Compare `.chars().count()` vs `.len()` on a Korean string to see the byte/char mismatch
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy