When I started learning Rust, I struggled a bit with choosing the right type for different situations. I think this comes from spending a lot of time working only with high-level programming languages like TypeScript. In TypeScript, a string is just a string, so there’s no need to overthink it.

Over time, I’ve developed my own rules based on my successes and failures. Recently, I came across a blog post by Steve Klabnik that I found really insightful. If you’re interested in a deeper explanation, I highly recommend checking it out. He presents some progressive, level-based rules for working with strings in Rust.

I don’t intend to transcribe the entire post here, but I’ll summarize the core rules that, as the author mentions, address the majority of the &str vs. String dilemma in Rust. It’s not too different from what I’ve been doing, but having your ideas validated (or refined) by someone more experienced definitely boosts your confidence.

Rule #1

Always use String in structs.

1struct Person {
2    name: String,
3}

Steve emphasizes in level 4 of his rules that there are exceptions where &str might be a better choice. However, he also mentions that if you find yourself wondering when to use it, it’s best to keep things simple and stick with String. At that point, you’re just not ready to deal with the complexity of storing a &str in a struct yet.

Rule #2

For functions, use &str for parameters and String types for return values.

A Rust code that uses only String looks like this:

 1fn first_word(words: String) -> String {
 2    words
 3        .split_whitespace()
 4        .next()
 5        .expect("words should not be empty")
 6        .to_string()
 7}
 8
 9fn main() {
10    let sentence = "Hello, world!";
11
12    println!("{}", first_word(sentence.to_string()));
13
14    let owned = String::from("A string");
15
16    println!("{}", first_word(owned.clone()));
17    println!("{}", first_word(owned));
18}

As you can see, on line 16, we needed to call owned.clone(). Otherwise, the next line would result in an ownership violation.

We can improve this, as shown in the code below:

 1fn first_word(words: &str) -> String {
 2    words
 3        .split_whitespace()
 4        .next()
 5        .expect("words should not be empty")
 6        .to_string()
 7}
 8
 9fn main() {
10    let sentence = "Hello, world!";
11
12    println!("{}", first_word(sentence));
13
14    let owned = String::from("A string");
15
16    println!("{}", first_word(&owned));
17    println!("{}", first_word(&owned));
18}

Now we declare a parameter of type &str on line 1. On lines 16 and 17, we can pass references as arguments, so there’s no need to use .clone() anymore.

Also, don’t worry about forgetting to use the & operator when calling the function; the Rust compiler won’t let you miss it. It will guide you with helpful messages and advice.

And that’s it! The author states (though empirically, without precise data) that these simple rules will cover about 95% of situations. That works great for me so far.