← Back to Rust series
🦀
Ownership
Ownership · Prerequisite: lesson 08 · Ownership · Prerequisite: lesson 09

09–10. Lifetimes — explicit 'a and elision rules · String and &str in Depth — UTF-8, indexing

A lifetime is a compile-time marker for how long a reference is valid. You write them as `'a`, but most of the time the compiler infers them through elision rules. This lesson covers why lifetimes exist, when to write them by hand, and the meaning of `'static`. Rust strings are UTF-8 encoded. `String` is a heap-owned mutable buffer, `&str` is a reference view into part of it. This lesson explains why integer indexing is intentionally disallowed and how `.chars()` / `.bytes()` / `.char_indices()` give you explicit control over Unicode iteration.

Rustlifetime'aelisionstaticreferenceString&strUTF-8Unicodechars
Duration
⏱ ~1.5 hours + ~1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Lesson 08
OUTCOME
A lifetime is a compile-time marker for how long a reference is valid. You write them as `'a`, but most of the time the compiler infers them through elision rules. This lesson covers why lifetimes exist, when to write them by hand, and the meaning of `'static`. / Rust strings are UTF-8 encoded. `String` is a heap-owned mutable buffer, `&str` is a reference view into part of it. This lesson explains why integer indexing is intentionally disallowed and how `.chars()` / `.bytes()` / `.char_indices()` give you explicit control over Unicode iteration.

What you'll learn

  • 1See why lifetimes prevent dangling references
  • 2Annotate functions and structs with `'a`
  • 3Recite the 3 lifetime elision rules
  • 4Distinguish `'static` from other lifetimes
  • 5Read and resolve lifetime-related compile errors
  • 6Describe the memory layouts of String and &str
  • 7Build Strings and extend them with `push_str` / `push` / `+`
  • 8Explain why integer indexing on strings is blocked
  • 9Pick between `.chars()` / `.bytes()` / `.char_indices()`
  • 10Compose strings with the `format!` macro

09. Lifetimes — explicit 'a and elision rules

A lifetime is a compile-time marker for how long a reference is valid. You write them as `'a`, but most of the time the compiler infers them through elision rules. This lesson covers why lifetimes exist, when to write them by hand, and the meaning of `'static`.

Overview

A reference whose target dies becomes a dangling reference. Rust prevents this at compile time by tracking lifetimes for every reference. Most code never spells them out — but functions that take references and return references sometimes need annotations.

Core Concepts

1) The problem lifetimes solve

rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;   // x is about to die...
    } // x dropped
    println!("{}", r); // error: `x` does not live long enough
}

2) Explicit annotation 'a

When a function takes references and returns one, the compiler needs to relate them. `'a` is any name (commonly a, b, c) marking a shared lifetime.

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

3) Elision — 3 rules

  1. Each reference parameter gets its own lifetime
  2. If there's one parameter, its lifetime is assigned to all output lifetimes
  3. If there's `&self` or `&mut self`, its lifetime is assigned to all output lifetimes

When inference succeeds via these rules, no annotation is needed — which is why everyday code rarely shows `'a`.

4) 'static — program-long

`&'static str` is valid for the program's entire run. String literals are the classic example.

Hands-on Examples

Two-input case that needs annotation:

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("short");
        result = longest(s1.as_str(), s2.as_str());
        println!("longest = {}", result);
    } // s2 dropped — result valid only inside the inner scope
}

A struct holding a reference must declare a lifetime:

rust
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let e = Excerpt { part: first_sentence };
    println!("{}", e.part);
}

Elision in action — no annotation needed:

rust
// elidable — one input ref, rule 2 applies
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &b) in bytes.iter().enumerate() {
        if b == b' ' { return &s[0..i]; }
    }
    s
}
// fully written: fn first_word<'a>(s: &'a str) -> &'a str { ... }

Common Mistakes

Q. How far should I spell lifetimes out?

A. Only when the compiler asks. Errors point out where elision can't decide — then add annotations there. No need to pre-emptively scatter `'a` everywhere.

Q. Is `'static` a magic catch-all?

A. No. `'static` means "lives forever" — that's a **strong constraint**. Short-lived references can't satisfy it. Sprinkling `'static` on signatures reduces caller flexibility a lot.

Q. What if two references have different lifetimes?

A. Declare both with `<'a, 'b>` and optionally an outlives bound `'b: 'a`. Usually you can simplify by tying both to the shorter of the two — when lifetime algebra gets ugly, your design probably needs a rethink.

Recap

  • Lifetimes mark how long references are valid for the compiler
  • Elision auto-fills most signatures
  • Annotations are needed when functions relate refs in to refs out
  • `'static` = whole program, not a free pass

Try It Yourself

  1. Try `longest` with two different lifetimes `'a, 'b` and read the error
  2. Define `struct Book<'a> { title: &'a str, author: &'a str }` and use it
  3. Write code that creates a dangling reference on purpose, then copy the compiler's exact message

10. String and &str in Depth — UTF-8, indexing

Rust strings are UTF-8 encoded. `String` is a heap-owned mutable buffer, `&str` is a reference view into part of it. This lesson explains why integer indexing is intentionally disallowed and how `.chars()` / `.bytes()` / `.char_indices()` give you explicit control over Unicode iteration.

Overview

If you're used to `s[0]` for the first character, Rust will feel awkward at first — that doesn't compile. The reason is honest: in UTF-8 a character is 1 to 4 bytes long, so an integer index is fundamentally ambiguous (bytes? characters?). Rust refuses to guess and gives you `.chars()` instead.

Core Concepts

1) String's memory layout

Internally a `Vec<u8>` — heap-owned, growable bytes. The bytes are UTF-8 encoded; methods enforce safe access.

2) &str's memory layout

(data pointer, byte length) — fat pointer. Can point to a String, a `'static` literal, or someone else's memory.

3) UTF-8 variable width

CharacterBytesExample
ASCII1'a' = 0x61
Korean / CJK3'한' = 0xED 0x95 0x9C
Some emoji4'😀' = 0xF0 0x9F 0x98 0x80

`s[0]` would be "first byte" or "first char" — ambiguous → compile error.

4) Iteration choices

  • **.chars()** — per-character (char)
  • **.bytes()** — per-byte (u8)
  • **.char_indices()** — (byte index, char) pairs

Hands-on Examples

Building and concatenating strings:

rust
fn main() {
    let mut s = String::new();
    s.push_str("hello");
    s.push(' ');
    s.push_str("world");
    println!("{}", s); // hello world

    let a = String::from("Hello, ");
    let b = String::from("world!");
    let c = a + &b;    // a is moved, b is borrowed
    println!("{}", c);
}

Per-character processing:

rust
fn main() {
    let s = "안녕 hi😀";
    println!("byte length: {}", s.len());            // 13
    println!("char count: {}", s.chars().count());   // 6
    for c in s.chars() { print!("[{}]", c); }
    println!();
}

`format!` — the idiomatic string builder:

rust
fn main() {
    let name = "Rust";
    let n = 22;
    let msg = format!("{} track {} lessons", name, n);
    println!("{}", msg);
}

Common Mistakes

Q. Why can't I grab the first character with s[0]?

A. You can't. Use `.chars().next().unwrap()` or `.chars().nth(0).unwrap()`. For pure ASCII you can index bytes with `.as_bytes()[0]`.

Q. Adding two Strings with + made the first one disappear

A. `+` takes ownership of the left side (move). To keep both alive, use `format!("{}{}", a, b)` or `.clone()`.

Q. The length of a Korean string looks wrong

A. `.len()` is **byte length**. For character count use `.chars().count()`. Mixing these up is a common source of panics in Korean text processing.

Recap

  • String = heap-owned Vec<u8>, &str = fat-pointer view
  • UTF-8 variable width means integer indexing is blocked on purpose
  • Use .chars() / .bytes() / .char_indices() for explicit iteration
  • `format!` is the idiomatic way to build a new String

Try It Yourself

  1. Read a Korean sentence and print byte length vs. character count
  2. Write `fn reverse(s: &str) -> String` using `.chars().rev()`
  3. Split a string on spaces and uppercase the first letter of each word
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