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.
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
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.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}3) Elision — 3 rules
- Each reference parameter gets its own lifetime
- If there's one parameter, its lifetime is assigned to all output lifetimes
- 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:
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:
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:
// 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
- Try `longest` with two different lifetimes `'a, 'b` and read the error
- Define `struct Book<'a> { title: &'a str, author: &'a str }` and use it
- 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
| Character | Bytes | Example |
|---|---|---|
| ASCII | 1 | 'a' = 0x61 |
| Korean / CJK | 3 | '한' = 0xED 0x95 0x9C |
| Some emoji | 4 | '😀' = 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:
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:
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:
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
- Read a Korean sentence and print byte length vs. character count
- Write `fn reverse(s: &str) -> String` using `.chars().rev()`
- Split a string on spaces and uppercase the first letter of each word
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