Strings Theory

Strings in Rust: Why Text Feels So Different from Numbers

Published on: 02 September 2025

Written by Michael Jendryke

Strings in Rust

Rust

coding

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?


Copy vs. Move: The Key Distinction

Let’s start with the most fundamental point: some types in Rust are Copy, and some are not.

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. Owned

Another major difference comes from the two main string types:

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.


Why .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:

You’ll also encounter String::from("..."), which is effectively the same as "..." .to_string() — just a different idiom.


.get() and Slicing Strings

Strings 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.


Cloning vs. Copying

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.


Structs and Strings

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.


Why Text Feels Different

So yes, text is different in Rust. Not because the designers wanted to make it harder, but because:

  1. Text is complicated (UTF-8, variable length, heap allocation).
  2. Rust makes ownership and copying explicit, so you see where the costs are.
  3. The language refuses to give you unsafe shortcuts (like indexing invalid UTF-8).

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.


A Mental Model

Here’s a simple way to keep it straight:


Conclusion

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.