String vs &str
Functional Programming
Tutorial
The Problem
Many languages have a single string type. Rust distinguishes between ownership and borrowing at the type level: String owns heap memory and can grow; &str is a fat pointer (address + length) into any UTF-8 bytes — a string literal in the binary, a slice of a String, or a network buffer. This design enables functions to accept both "literals" and String values without copying, a guarantee enforced at compile time rather than at runtime.
🎯 Learning Outcomes
String as Vec<u8> with a UTF-8 invariant versus &str as a borrowed view&str to work with both String and string literalsString::from / .to_string() / format! to create owned stringsString to obtain a &str with the same lifetimefirst_word using byte-level find to return a slice of the inputCode Example
#![allow(clippy::all)]
// 471. String vs &str: ownership semantics
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn make_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
fn first_word(s: &str) -> &str {
&s[..s.find(' ').unwrap_or(s.len())]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let s = String::from("test");
let g = make_greeting(&s);
assert_eq!(g, "Hello, test!");
}
#[test]
fn test_literal() {
assert_eq!(make_greeting("hi"), "Hello, hi!");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
}Key Differences
String is uniquely owned and freed when it goes out of scope; OCaml strings are garbage-collected — ownership is irrelevant.&str slices point into existing memory; OCaml String.sub always copies.String is mutable via push_str/push; OCaml string is immutable — mutation requires Bytes.t.&String coerces to &str automatically; OCaml has no such coercion — you pass string values directly.OCaml Approach
OCaml's string is an immutable byte sequence; Bytes.t is the mutable counterpart. There is no ownership distinction at the type level:
let greet name = Printf.printf "Hello, %s!\n" name
let make_greeting name = "Hello, " ^ name ^ "!"
let first_word s =
match String.index_opt s ' ' with
| Some i -> String.sub s 0 i
| None -> s
String.sub always allocates a new string; there is no zero-copy slice type in the standard library (Bigstring from core provides views for I/O).
Full Source
#![allow(clippy::all)]
// 471. String vs &str: ownership semantics
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn make_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
fn first_word(s: &str) -> &str {
&s[..s.find(' ').unwrap_or(s.len())]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let s = String::from("test");
let g = make_greeting(&s);
assert_eq!(g, "Hello, test!");
}
#[test]
fn test_literal() {
assert_eq!(make_greeting("hi"), "Hello, hi!");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let s = String::from("test");
let g = make_greeting(&s);
assert_eq!(g, "Hello, test!");
}
#[test]
fn test_literal() {
assert_eq!(make_greeting("hi"), "Hello, hi!");
}
#[test]
fn test_first_word() {
assert_eq!(first_word("hello world"), "hello");
assert_eq!(first_word("single"), "single");
}
}
Exercises
longest_word(s: &str) -> &str that returns the longest whitespace-delimited word as a slice without allocating.criterion to measure the cost of .to_string() vs. String::from() vs. format!("{}", s) for a 100-byte input.impl AsRef<str>**: Rewrite greet to accept impl AsRef<str> and verify it works with String, &str, and Cow<str>.