Rust - The differences between the &str and String types
Photo by Kurt Cotoaga
To inaugurate my new blog, let’s talk about Rust!
I want to talk about the differences between &str and String because I know
many people who are learning Rust and struggle to understand what are the differences
between these types and when to use them.
It’s most difficult for people who have a background in languages where this kind of difference doesn’t exist - like in JavaScript and Python.
So, the objective is to try to explain it in a very simple way.
Please note that we won’t get into too many details about the memory model of String
vs &str as this post is more focused on beginners that are trying Rust for the
first time.
The Problem
You have finally decided to write your first Rust program and become a 🦀 like the rest of us.
You decide to write a simple program, that says “Good morning!” to the user, it cannot be that difficult right? You just need a function to print that.
You are used to writing JavaScript with some TypeScript, so you know you need
a function that takes as a parameter a string and concatenates that param with
your “Good Morning, {name}”. After checking the Rust docs, you see that there
is a parameter called String that seems perfect for your usecase.
So, you go ahead and open your super text editor (Neovim 😜) and write this simple program:
fn main() {
    let name = "John";
    good_morning(name);
}
fn good_morning(name: String) {
   println!("Good Morning, {name}");
}
You go to your terminal and try to run this program with cargo run but it
doesn’t compile 😅
Why is that?
Luckily, the Rust compiler is very helpful and gives us an error message that describes the problem:
error[E0308]: mismatched types
 --> src/main.rs:3:18
  |
3 |     good_morning(name);
  |     ------------ ^^^^- help: try using a conversion method: `.to_string()`
  |     |            |
  |     |            expected struct `String`, found `&str`
  |     arguments to this function are incorrect
  |
note: function defined here
 --> src/main.rs:6:4
  |
6 | fn good_morning(name: String) {
  |    ^^^^^^^^^^^^ ------------
For more information about this error, try `rustc --explain E0308`.
It is saying that it was expecting a string of type String but that
we passed something of type &str 🤔
But what the heck is that? Your variable name seems to be a perfectly normal
string, why the compiler is not happy?
Well, let’s try to understand what is going on.
The String type
The String type is in a conceptually very similar to the string type in
JavaScript/TypeScript and other dynamic programming languages.
An element of type String can do almost all string operations that
we find in other languages.
Strings are always valid UTF-8 and can grow in size, meaning they are perfect
if you need to manipulate dynamic strings.
There are 3 main ways to create a String in Rust:
let x = String::from("Hi there!");
let y = "Hi there!".to_string();
let z = "Hi there!".to_owned();
As mentioned above, String types are growable meaning that you can append a
char or a &str to it:
// Note that we need the `mut` keyword here
// to indicate that this is a mutable variable
let mut x = String::from("Foo");
println!("{x}");
// prints:
// Foo
// In Rust, to represent a `char`, you need to
// put it inside single quotes.
x.push('!');
println!("{x}");
// prints:
// Foo!
x.push_str("Bar");
println!("{x}");
// prints:
// Foo!Bar
As we can see, a String is very flexible and it shines when we need to
store text that is unknown at compile time or that needs to be manipulated.
These properties are directly linked to how String is stored in our computer’s
memory: A String is heap-allocated and it’s stored as a vector of bytes (Vec<u8>).
In Rust, when you define a string only with double quotes, you are actually
defining a string slice of type &str and not a String.
So, you might be wondering how we could fix our good_morning function to make
it works with a String, we need to do it like that:
fn main() {
    let name = String::from("John");
    good_morning(name);
}
fn good_morning(name: String) {
   println!("Good Morning, {name}");
}
But ok, if String are dynamically heap allocated strings, what is &str?
The &str type
The &str type (pronounced string slice) type is an immutable string, which
its size is known at compile time and is not dynamically growable like the String
type.
&str is way more efficient in terms of memory usage than the String type and
it’s the preferred string type for a text that is known at compile time and doesn’t need
to be mutated. It has fewer “features” than String but it has better performance.
All string literals in Rust (e.g., “Rust is awesome”) are of type &str, meaning
that to create a &str you just need to write the text between double quotes:
let s = "Rust is awesome";
// type is `&str`
However, as the size of &str is known at the compiled time, you cannot append chars
or &str to it. In other words, it can’t be modified.
If you need to modify a &str, you need to first convert it to String:
let s = "Rust is awesome";
// we could have directly used the method `to_string` in the line just above
// but bare with me for the example
let mut s_converted = String::from(s);
s_converted.push('!');
println!("{s_converted}");
// prints: Rust is awesome!
String slices are also very useful when you want a reference to a String that
is “owned” by someone else but you don’t want to copy/clone the string and allocate
more memory to use it. If we go back to our good_morning function, we can see
how this pattern can be useful:
fn main() {
    let name = String::from("John");
    // Here we are now passing a 'reference' (that is what the '&' means)
    // to our `good_morning` function
    good_morning(&name);
}
// The function now takes as a param `&str`
fn good_morning(name: &str) {
   println!("Good Morning, {name}");
}
When passing strings between functions, &str is often the preferred type,
as it can be passed as a reference and doesn’t require an allocation on the heap.
Another difference is that &str contains a pointer to the start of the string
and the length of the string/slice. This can point to anything, like something
in the stack, in the heap, static memory and etc. A String as mentioned before
is heap allocated and dynamically allocated.
It brings us to a very interesting conclusion that is: the conversion of String
to &str is very cheap, as it will be just a reference to the data,
but the opposite is not because we will need to allocate memory in the heap for that.
What are the usecases for String and &str?
As a rule of thumb, I always try to use &str when the string is known at compile
time and I don’t need to mutate or own it. A good example is our good_morning function.
But let’s say that our good_morning function parameter name is not coming
from our code but from an external API that gets user input from a web page. In
this case, we know that name cannot be known at compile time and in this case
we need to use String.
Another good use case for String is when you need to mutate and manipulate the
string.
In conclusion, the choice between String and &str depends on the specific needs of a program.
&str is the preferred choice for strings that are known at compile time and don’t require any mutations,
as it offers better performance and is more efficient in terms of memory usage.
On the other hand, String is the better option for strings that are not known
at compile time or require mutations, as it allows for the dynamic allocation of
memory and has more features for string manipulation.
Ultimately, the choice between the two types should be made based on the specific requirements of the program and the desired trade-off between performance and functionality.