Skip to content
GitHub Twitter

Rust - The differences between the &str and String types

snow covered mountain in reflective photography 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.