Rust Has TWO String Types and I'm Not Okay π¦π€
Rust Has TWO String Types and I'm Not Okay π¦π€
Rite of passage: Every developer learning Rust hits the same wall. Not the borrow checker. Not lifetimes. Not Fn vs FnMut. No, the wall most people crash into first is this compiler error:
expected `&str`, found `String`
And then they go: "Wait. There are TWO string types?? WHY?!"
Coming from 7 years of Laravel and Node.js, I had exactly one mental model for strings: a string is a string. In PHP, "hello" is a string. In JavaScript, "hello" is a string. You pass it around. You concatenate it. You move on with your life.
Then Rust handed me String AND &str and said "pick one." And I said "they look the same to me." And the compiler said "they are NOT the same." And we went back and forth like this for about three days. π€
Let me save you those three days.
The 30-Second Version π―
Here's the mental model that finally made it click for me:
String = an owned, growable string (lives on the heap, you control it)
&str = a borrowed view into some string data (you're just looking, not owning)
Coming from PHP: String is like a $string variable you own and can modify. &str is like passing $string by reference for read-only access β except Rust enforces the read-only part at compile time and tracks who owns the original.
Still fuzzy? Let's go deeper.
Why Does This Even Exist? π€
In PHP, every string is a copy. You assign a string, PHP copies it. Pass it to a function, PHP copies it. This is simple! Convenient! And also involves a LOT of hidden memory allocation behind the scenes that you never see.
Rust has no garbage collector and no hidden copies. Every byte of memory has exactly one owner, and the compiler knows where it is at all times. To make strings work in this world, Rust needs to distinguish between:
- "I OWN this string data and I'm responsible for freeing it" β that's
String - "I'm just LOOKING at some string data that someone else owns" β that's
&str
// String: allocated on the heap, YOU own it
let owned: String = String::from("hello, world");
// &str: a "window" into the owned string (or into static memory)
let borrowed: &str = &owned; // just borrowing a view
// String literals are &str too!
let literal: &str = "hello, world"; // stored in the binary, lives forever
What excited me about this: For my RF/SDR projects, I'm processing millions of string-like byte sequences from radio protocols. Understanding the difference between owned buffers and borrowed slices was the key to writing signal parsers that don't copy data unnecessarily. Once you get String vs &str, you get ALL of Rust's slice types. π‘
Let's Break the Confusion with PHP π
Here's what I wish someone had shown me on day one:
// PHP: just... strings
function greet($name) {
echo "Hello, " . $name . "!\n";
}
greet("Alice"); // fine
$someone = "Bob";
greet($someone); // also fine, PHP doesn't care
// Rust: you need to know if you're taking ownership or borrowing
fn greet(name: &str) { // &str = "just borrow a view of the string"
println!("Hello, {}!", name);
}
greet("Alice"); // string literals are &str β works!
let someone = String::from("Bob");
greet(&someone); // & borrows the String as &str β works!
greet(someone); // ERROR: passing an owned String where &str expected
The golden rule: If you're just reading a string inside a function, take &str. If you need to keep or modify the string, take String. When in doubt, &str is almost always what you want for function parameters.
The Four Ways to Make a String (It's Really Not That Bad) βοΈ
// 1. String literals β always &str, always valid for the whole program
let a: &str = "I live forever in the binary";
// 2. String::from() β converts a &str into an owned String
let b: String = String::from("Now I live on the heap");
// 3. .to_string() β same thing, different syntax
let c: String = "Also heap-allocated now".to_string();
// 4. format!() β like sprintf in PHP
let d: String = format!("Hello, {}!", "world"); // always returns String
Coming from PHP: format!() is your PHP string interpolation. String::from() is you explicitly saying "I need to own this." &str is you saying "I just need to read this."
The Coercion Magic That Saves Your Sanity β¨
Here's the part where Rust is actually KIND:
fn print_it(s: &str) {
println!("{}", s);
}
let owned = String::from("I am owned");
// Rust automatically "coerces" String β &str when needed!
print_it(&owned); // & borrows owned as &str β works!
print_it("literal"); // literals are &str β works!
Rust has a feature called "deref coercion" that automatically converts &String β &str. This means functions that accept &str work with BOTH string literals AND owned Strings. This is why &str is almost always the right choice for function parameters β it accepts everything.
The analogy that clicked for me: &str is like a USB-C port. Both string literals (the charger that comes in the box) and String (your fancy third-party cable) plug into it just fine. String is the specific cable β great to own, annoying to require.
When Do You NEED String? π
Good question. These are the cases where you actually need the owned version:
// 1. Building a string dynamically (can't do this with &str!)
let mut result = String::new();
result.push_str("Hello");
result.push_str(", ");
result.push_str("world!");
// result is "Hello, world!" β try doing this with a &str!
// 2. Returning a string from a function that creates it
fn get_station_name(frequency: f32) -> String {
format!("{:.1} MHz FM", frequency) // must be owned β you're creating it!
}
// 3. Storing a string in a struct (structs need owned data)
struct RadioStation {
name: String, // owned β the struct OWNS this string
frequency: f32,
}
For my RF/SDR hobby: Parsing radio protocol messages means building strings from raw bytes on the fly. That's a String job β I'm creating data, not borrowing it.
The Struct Gotcha Every PHP Developer Hits π
PHP doesn't make you think about this. Rust does.
// BROKEN: can't store &str in a struct without lifetime annotations
struct StationLog {
last_message: &str, // ERROR: missing lifetime specifier
}
// FIXED option 1: use String (simplest, most common)
struct StationLog {
last_message: String, // owned β easy, no lifetime needed
}
// FIXED option 2: use &str with a lifetime annotation (advanced)
struct StationLog<'a> {
last_message: &'a str, // borrowed from somewhere that outlives us
}
My advice: Start with String in structs. Always. You can optimize to &str with lifetime annotations later, once you understand what's happening. Don't let lifetime annotations scare you away from structs.
The Comparison That Made Me Laugh Out Loud π
// PHP: building a string for a radio protocol message
function buildMessage(string $callsign, string $data): string {
return $callsign . ": " . $data;
}
// You never think about WHERE that string lives. PHP just... deals with it.
$msg = buildMessage("KP2AKP", "signal report 59");
// Rust: exact same function
fn build_message(callsign: &str, data: &str) -> String {
format!("{}: {}", callsign, data)
}
// Now you know EXACTLY where it lives:
// - callsign and data are borrowed references (no allocation!)
// - the return value is freshly allocated on the heap (you own it!)
let msg = build_message("KP2AKP", "signal report 59");
PHP wins on "I don't want to think about this." Rust wins on "now you KNOW what your code costs." For high-throughput signal parsing where every allocation matters, knowing the cost is a feature, not a bug. π―
The Rule of Thumb That Works 99% of the Time π
I made myself a mental checklist, and it's served me well:
Am I receiving a string to READ? β use &str (borrow it)
Am I creating or MODIFYING a string? β use String (own it)
Am I storing a string in a struct? β use String (own it)
Am I returning a string I just made? β return String (you made it, you own it)
That's it. That's the whole decision tree.
// Applying the rule:
fn process_signal(raw_data: &str) -> String { // read &str, return String
let cleaned = raw_data.trim(); // &str (just a view of raw_data)
let mut result = String::new(); // String (I'm building something)
result.push_str("[");
result.push_str(cleaned);
result.push_str("]");
result // return the owned String
}
TL;DR: The Three Things to Remember π
&strfor function parameters β accepts both literals andString, borrow don't ownStringfor return values, structs, and dynamic strings β you own it, you built it&owned_stringconvertsStringβ&strβ the&is your adapter
Stop fighting the compiler. The moment you internalize "am I OWNING this or BORROWING it?", String vs &str goes from Rust's most confusing feature to one of its most elegant.
Coming from 7 years of PHP where strings just materialize and disappear by magic β understanding Rust strings taught me more about how computers actually work than a decade of PHP ever did. The GC was hiding all the complexity. Rust just... shows you the map. πΊοΈ
And once you have the map, you can navigate. π¦
Still arguing with the Rust compiler about strings? Find me on LinkedIn β I've lost every argument with the borrow checker so you don't have to!
Want to see &str and String in action for RF protocol parsing? Check out my GitHub for SDR hobby projects where string allocation is a real concern.
Now go own your strings. Or borrow them. You know the difference now. π¦π€