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.
String
s 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.