113: String vs &str
Difficulty: 2 Level: Intermediate `String` is an owned, heap-allocated string you can grow; `&str` is a borrowed view into any string data โ use `&str` in function parameters to accept both without allocating.The Problem This Solves
In C, strings are `char*` โ a raw pointer. Owned, borrowed, literal, stack-allocated โ the type doesn't tell you. You discover which it is when you try to `free()` the wrong one and the program crashes, or when you modify a string literal and get a segfault. In Java, `String` is always heap-allocated and immutable. Simple โ but you can't have a "view into part of a string" without copying. Every substring allocates. String-heavy code in Java has serious GC pressure. OCaml's `string` type is similarly a single concept: immutable, GC-managed. No distinction between owned and borrowed. Clean, but at the cost of GC overhead on all strings. Rust has two types because they solve different problems. `String` is owned heap-allocated data โ you can push, truncate, and pass it around while keeping ownership clear. `&str` is a borrowed slice โ a pointer and a length pointing into any string data (a `String`, a string literal baked into the binary, or a substring). No allocation. Pass `&str` to functions and you never force the caller to allocate.The Intuition
`String` owns the text on the heap; `&str` borrows a view into any existing text โ using `&str` in function signatures lets callers pass string literals, slices, or `String` references without forcing an allocation.How It Works in Rust
// String: owned, heap-allocated, growable
let mut owned: String = String::from("hello");
owned.push_str(", world"); // grows in-place
owned.push('!');
println!("{}", owned); // "hello, world!"
// &str: borrowed view โ no allocation
let borrowed: &str = "hello, world!"; // points into binary (static lifetime)
let slice: &str = &owned[0..5]; // points into the String
// Function that accepts both &str and &String
// BAD: fn greet(name: &String) โ forces callers to have a String
// GOOD: fn greet(name: &str) โ works with literals AND String
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn demo() {
let owned = String::from("Alice");
greet(&owned); // coerces &String to &str automatically
greet("Bob"); // string literal is already &'static str
greet(&owned[0..3]); // slice of String โ also &str
}
// Converting between them
let s: String = "hello".to_string(); // &str โ String (allocates)
let s: String = String::from("hello"); // same
let r: &str = &s; // String โ &str (no allocation)
let r: &str = s.as_str(); // explicit
// String owns its buffer; dropping String frees the memory
// &str borrows โ it can't outlive the data it points to
fn returns_str() -> &'static str {
"static lifetime โ baked into binary"
}
// This would be a compile error:
// fn returns_local_str() -> &str {
// let s = String::from("temporary");
// &s // ERROR: s is dropped, &s would dangle
// }
What This Unlocks
- Zero-copy function APIs โ `fn process(s: &str)` accepts string literals, borrowed `String`s, and substrings without any caller-side allocation.
- Substring views โ `&s[start..end]` gives a zero-copy view into any part of a string; essential for parsers and text processing.
- Clarity about ownership โ `String` in a struct means "I own this text." `&'a str` in a struct means "I borrow this text from somewhere that must outlive me."
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| String type | Single `string` (immutable, GC) | `String` (owned) and `&str` (borrowed) |
| Substring | `String.sub` โ always copies | `&s[start..end]` โ zero-copy view |
| String literal type | `string` | `&'static str` |
| Function parameter | `string` โ always owned ref | Prefer `&str` โ accepts all string kinds |
| Mutability | Immutable (use `Bytes` for mutable) | `String` is mutable; `&str` is read-only |
// Example 113: String vs &str
//
// String: owned, heap-allocated, growable, mutable
// &str: borrowed slice, usually a view into a String or string literal
// Approach 1: String creation and conversions
fn approach1() {
// String literals are &'static str
let literal: &str = "hello";
// Convert to owned String
let mut owned: String = String::from("hello");
owned.push_str(" world");
// &str from String
let slice: &str = &owned;
let upper = slice.to_uppercase(); // returns new String
assert_eq!(upper, "HELLO WORLD");
println!("Literal: {}, Owned: {}, Upper: {}", literal, owned, upper);
}
// Approach 2: Functions that accept both
fn first_word(s: &str) -> &str {
s.split(',').next().unwrap_or(s).trim()
}
fn char_count(s: &str) -> usize {
s.chars().count()
}
fn approach2() {
// Works with &str
let word = first_word("hello, world!");
assert_eq!(word, "hello");
// Works with &String (auto-deref)
let owned = String::from("goodbye, moon!");
let word2 = first_word(&owned);
assert_eq!(word2, "goodbye");
println!("word: {}, word2: {}, chars: {}", word, word2, char_count(word));
}
// Approach 3: Building strings efficiently
fn approach3() {
// With push/push_str
let mut s = String::with_capacity(50);
s.push_str("Hello");
s.push(' ');
s.push_str("World");
// With format!
let formatted = format!("{} - {}", s, 2024);
// With collect
let collected: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
// With join
let joined = ["one", "two", "three"].join(", ");
assert_eq!(s, "Hello World");
assert_eq!(collected, "HELLO");
println!("{} | {} | {} | {}", s, formatted, collected, joined);
}
fn main() {
println!("=== Approach 1: String vs &str ===");
approach1();
println!("\n=== Approach 2: Accept Both ===");
approach2();
println!("\n=== Approach 3: Building Strings ===");
approach3();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_str_to_string() {
let s: &str = "hello";
let owned: String = s.to_string();
assert_eq!(s, owned.as_str());
}
#[test]
fn test_string_slicing() {
let s = String::from("hello world");
let hello: &str = &s[..5];
assert_eq!(hello, "hello");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("a, b"), "a");
assert_eq!(first_word("single"), "single");
}
#[test]
fn test_string_building() {
let mut s = String::new();
s.push_str("abc");
s.push('d');
assert_eq!(s, "abcd");
assert_eq!(s.len(), 4);
assert_eq!(s.capacity() >= 4, true);
}
#[test]
fn test_utf8() {
let s = "hรฉllo";
assert_eq!(s.len(), 6); // bytes
assert_eq!(s.chars().count(), 5); // characters
}
}
(* Example 113: String vs &str โ OCaml string โ Rust Two String Types *)
(* OCaml has one string type. Rust has two: String (owned, heap) and &str (borrowed slice). *)
(* Approach 1: String creation and manipulation *)
let approach1 () =
let s = "hello" in
let s2 = s ^ " world" in
let upper = String.uppercase_ascii s2 in
assert (upper = "HELLO WORLD");
Printf.printf "Original: %s, Upper: %s\n" s2 upper
(* Approach 2: Substring operations *)
let approach2 () =
let s = "hello, world!" in
let sub = String.sub s 7 5 in
let first_word = match String.index_opt s ',' with
| Some i -> String.sub s 0 i
| None -> s in
assert (sub = "world");
assert (first_word = "hello");
Printf.printf "Sub: %s, First word: %s\n" sub first_word
(* Approach 3: String as bytes *)
let approach3 () =
let s = "Rust" in
let bytes = Bytes.of_string s in
Bytes.set bytes 0 'r';
let modified = Bytes.to_string bytes in
assert (modified = "rust");
Printf.printf "Modified: %s\n" modified
let () =
approach1 ();
approach2 ();
approach3 ();
Printf.printf "โ All tests passed\n"
๐ Detailed Comparison
Comparison: String vs &str
String Creation
OCaml:
๐ช Show OCaml equivalent
let s = "hello" (* string literal *)
let s2 = s ^ " world" (* concatenation โ new string *)
Rust:
let s: &str = "hello"; // &'static str
let s2: String = format!("{} world", s); // owned String
let s3 = String::from("hello"); // also ownedFunction Parameters
OCaml:
๐ช Show OCaml equivalent
let greet name = Printf.printf "Hello, %s!\n" name
let () = greet "world" (* just one string type *)
Rust:
fn greet(name: &str) { println!("Hello, {}!", name); }
greet("world"); // &str โ
greet(&String::from("world")); // &String auto-derefs to &str โMutability
OCaml:
๐ช Show OCaml equivalent
let bytes = Bytes.of_string "hello" in
Bytes.set bytes 0 'H';
Bytes.to_string bytes (* "Hello" *)
Rust:
let mut s = String::from("hello");
s.push_str(" world"); // mutate in-place
s.replace_range(0..1, "H");
assert_eq!(s, "Hello world");