String vs &str in Rust
Rust's two string types, when to use each, and what the difference has to do with memory.
Rust has two main string types, and when you're coming from languages with one string type it's confusing as hell. This is what I learned building an in-memory key-value store.
# String, the owned type
String is heap-allocated and growable. It owns the underlying data and is responsible for cleaning it up when it goes out of scope.
let mut s = Stringfrom;
s.push_str; // Can modify, it owns the data
// s is automatically freed when it goes out of scope
You use String when you create stuff at runtime (user input, a request, etc.), or you need to mutate it, or it has to live beyond the current function.
# &str, the borrowed type
&str is a string slice, just a view into string data. It's borrowed because it doesn't own the data, it's only a pointer to the start plus the length.
let s = Stringfrom;
let slice: &str = &s; // "hello", just a view
Since it's borrowed, the compiler won't let you use it after the owner goes out of scope:
&str is for when you're reading, analyzing or parsing a string without modifying it, and for function parameters.
# Why I cared
In my key-value store I parse commands from TCP requests. The request buffer is temporary (arena-allocated), but stored values have to live permanently.
// Parse from temporary request buffer (&str)
// Store needs owned data (String)
If I tried to store &str references into the request buffer I'd have dangling pointers as soon as the request ends, and the compiler catches that at compile time.
Size-wise, String is 24 bytes on 64-bit systems:
- Pointer to heap data (8 bytes)
- Length (8 bytes)
- Capacity (8 bytes)
&str is 16 bytes:
- Pointer to data (8 bytes)
- Length (8 bytes)
No capacity because it can't grow, it's read-only.
Converting between them:
let owned = Stringfrom;
let borrowed: &str = &owned; // String -> &str (cheap)
let borrowed = "hello"; // &str (string literal)
let owned = borrowed.to_string; // &str -> String (allocates)
String literals like "hello" are &str by default, they point to data baked into the binary at compile time.
The distinction felt annoying at first, but it forces you to think about who owns the memory. After dealing with use-after-free bugs in Zig (where you track all of this in your head), having the compiler enforce it is kind of... nice.