Strings in Rust: Why Text Feels So Different from Numbers
Published on: 02 September 2025
Written by Michael Jendryke
When you first learn Rust, you quickly notice that some types behave very simply — like i32
, u64
, or f64
. You can assign them, copy them, add them, and pass them around with almost no friction.
But then you meet text. Suddenly you’re juggling String
, &str
, .to_string()
, .clone()
, .get()
, and ownership rules that make strings feel way more complex than numbers.
Why is text in Rust different? And how should we think about working with it?
Let’s start with the most fundamental point: some types in Rust are Copy
, and some are not.
Numbers (i32
, f64
, bool
, etc.) implement the Copy
trait. This means assigning them to a new variable copies the value instead of moving ownership.
let x = 42;
let y = x; // both x and y are valid, both hold 42
String
, on the other hand, is not Copy
. It’s an owned heap-allocated type. When you assign a String
to another variable, ownership moves:
let s1 = String::from("hello");
let s2 = s1; // s1 is now invalid
The Rust Book explains why:
“Rust will never automatically create ‘deep’ copies of your data. Therefore, any automatic copying can be assumed to be inexpensive.” (The Rust Programming Language, Chapter 4.1)
Numbers are cheap to copy — a few bytes on the stack. Strings, however, are pointers to heap-allocated memory, and blindly copying that pointer would cause two owners of the same memory, which is unsafe.
&str
vs. String
: Slice vs. OwnedAnother major difference comes from the two main string types:
&str
(string slice): a borrowed view into some UTF-8 text. It doesn’t own the data. Examples: string literals like "hello"
are &'static str
.String
: an owned, growable UTF-8 string allocated on the heap.The Book puts it this way:
“
String
is a growable, mutable, owned, UTF-8 encoded string type.” (The Rust Programming Language, Chapter 8.2)
Think of &str
like a window into text, and String
as the actual house.
.to_string()
?So what’s the deal with .to_string()
?
to_string()
converts a string slice (&str
) into an owned String
:
let slice: &str = "world";
let owned: String = slice.to_string();
Why not just use &str
everywhere? Because sometimes you need ownership. For example:
String
.String
.You’ll also encounter String::from("...")
, which is effectively the same as "..." .to_string()
— just a different idiom.
.get()
and Slicing StringsStrings in Rust are UTF-8 encoded. This means you can’t index into them directly with s[0]
, because one character may take multiple bytes. Instead, you use methods like .get()
:
let s = String::from("hello");
let h = s.get(0..1); // Option<&str>, Some("h")
Why does .get()
return an Option
? Because you might accidentally slice in the middle of a UTF-8 code point, which would be invalid. Rust refuses to let you create invalid text slices.
This is one of the ways Rust protects you from common text-handling pitfalls in other languages.
Because String
is not Copy
, if you want two owned values with the same text, you need to explicitly clone:
let s1 = String::from("hello");
let s2 = s1.clone(); // now both s1 and s2 own "hello"
This is more expensive than copying an i32
, because it duplicates the heap data. That’s why Rust forces you to spell it out with .clone()
.
Numbers, by contrast, don’t need .clone()
:
let x = 5;
let y = x; // just copies a 32-bit value
The rule of thumb: if a type owns heap data, you’ll need to clone
when you want two copies. If it’s just stack data, it’s usually Copy
.
This distinction becomes especially important with structs. Consider:
struct User {
name: String,
age: u32,
}
let u1 = User {
name: String::from("Alice"),
age: 30,
};
let u2 = u1; // ownership of u1.name moves, u1 is invalid
The entire struct is moved because one of its fields (name
) is not Copy
.
If User
had only Copy
fields (like numbers), then the whole struct would be Copy
too, and you could freely assign it.
So yes, text is different in Rust. Not because the designers wanted to make it harder, but because:
The result is a system that feels strict at first but keeps you safe from dangling pointers, double frees, or invalid strings — problems that plague C, C++, and even some higher-level languages.
Here’s a simple way to keep it straight:
Copy
, no ownership drama.Copy
, ownership matters.&str
: borrowed view into a string, cheap and flexible..to_string()
: turn a borrowed string slice into an owned String
..clone()
: duplicate the underlying heap data of a String
..get()
: safely slice strings without breaking UTF-8.Rust makes text handling explicit because it matters: text is big, shared, mutable, and potentially unsafe if handled carelessly. By forcing you to think about ownership and borrowing, Rust helps you write programs that are both efficient and correct.
So next time you find yourself muttering “why can’t I just treat String
like an i32
?”, remember: the rules are stricter for a reason. Text isn’t just bytes — it’s structured, variable, and shared — and Rust gives you the tools to manage it without hidden surprises.