Hello! Let's get back to building our chess game.
In the last lesson, we covered the basics of variables (let, mut) and functions (fn). You learned that Rust favors immutability and requires explicit type annotations for function signatures. This foundation is crucial as we move into more complex topics.
Today, we're going to tackle one of the most common points of confusion for newcomers to Rust: strings. In many languages like Java or C, you might be used to a single primary string type. Rust, however, has two: String and &str. Understanding the difference is key to writing efficient and correct Rust code.
This lesson will help you differentiate between these two types and learn how to convert between them. This knowledge is a gentle introduction to Rust's core concepts of data ownership and borrowing, which we will explore fully in the next lesson.
1. The Two String Types: An Overview
At a high level, the difference can be summarized like this:
String: An owned, mutable, growable string. It's stored on the heap, which means its size can change at runtime. You useStringwhen you need to create or modify string data, like building a player's name from user input.&str(pronounced "string slice"): A borrowed, immutable "view" or "slice" into string data. It's a reference to a sequence of characters that is stored somewhere else—either in the program's binary, on the heap inside aString, or on the stack. You use&strwhen you just need to read string data without taking ownership of it.
Think of it this way: a String is like owning the original, editable document file on your computer. A &str is like a read-only link to that document that you can share with others.
This diagram gives a great visual breakdown of how these types are represented in memory.

The most common place you'll see a &str is with string literals, the hardcoded strings you type directly in your code.
// This is a string literal. Its type is &'static str.
// The text "Hello, world!" is stored directly in our compiled program's binary.
let my_slice = "Hello, world!";
2. Understanding Memory and "Views"
To really grasp the difference, it's helpful to think about what's happening in memory. The following video provides a clear, concise explanation.
Understanding Strings in Rust - String vs &str
This video by Federico Terzi offers an excellent conceptual walkthrough of how String and &str relate to memory and the idea of a 'view' into data.
Watch the entire video (it's about 6 minutes). Pay close attention to: The distinction between heap and stack memory (00:36). How a &str acts as a 'view' without needing a new memory allocation (01:34). The practical advice on using &str for function parameters (05:41).
As the video explained, using &str is highly efficient because it avoids allocating new memory when you just need to pass string data to a function. This is a very common pattern in Rust.
3. Converting Between String and &str
Since you'll be working with both types, you need to know how to convert between them. This is a very common task in Rust programming. The article below gives a straightforward guide with code examples.
In-Depth Guide to Working with Strings in Rust
This guide from DEV Community provides clear, practical examples for converting between String and &str.
Read sections 1 and 2, including the 'Summary of Common Conversions'. This will walk you through the main methods for converting between the two types.
Now, let's put this into practice.
From a Slice to an Owned String (&str -> String)
When you have a &str (like a string literal) and you need an owned String that you can modify or give to another part of the program, you create a new String. This process allocates memory on the heap and copies the data.
You can do this in two main ways:
- Using the
.to_string()method. - Using the
String::from()function.
fn main() {
// This is a string slice (&str)
let move_notation_slice = "e2e4";
// Convert it to an owned String using .to_string()
let owned_move_1 = move_notation_slice.to_string();
// Convert it to an owned String using String::from()
let owned_move_2 = String::from(move_notation_slice);
// Now these are both of type String, and we can modify them
println!("Owned string 1: {}", owned_move_1);
println!("Owned string 2: {}", owned_move_2);
}
From an Owned String to a Slice (String -> &str)
This is the more common and efficient direction. If you have a String but a function only needs to read its contents, you can pass a slice (&str) to it. This is cheap because it doesn't allocate any new memory.
You can get a &str from a String by using the & operator, which creates a reference.
fn main() {
let player_name = String::from("Magnus");
// Pass a slice of the String to our function
print_greeting(&player_name);
// We can still use player_name here because we only 'lent' it to the function
println!("Welcome, {}!", player_name);
}
// This function takes a string slice.
// It can accept both a &str literal and a reference to a String.
fn print_greeting(name: &str) {
println!("Hello, {}! It's your turn.", name);
}
Run this code. Notice how print_greeting works with &player_name. Rust automatically converts &String into &str for you in this situation, a feature called deref coercion. This makes writing functions flexible.
Hands-On Activity
Let's apply this to our rust_chess project. We'll simulate getting player names as owned Strings and then passing them to a function that prints a welcome message.
- Open your
src/main.rsfile. - Delete any previous code from the
mainfunction. - Define two
Stringvariables,player1_nameandplayer2_name, initialized with some names. - Write a function
welcome_playerthat accepts one parameter,name, of type&str. This function should print a welcome message, like "Welcome to the game, [name]!". - In your
mainfunction, callwelcome_playerfor both players. You'll need to pass a reference to each name. - Run
cargo runto see the output.
Your final code might look something like this:
fn main() {
let player1_name = String::from("Player 1");
let player2_name = "Player 2".to_string(); // Just to show both methods work!
println!("Setting up the game...");
welcome_player(&player1_name);
welcome_player(&player2_name);
}
fn welcome_player(name: &str) {
println!("Welcome to the game, {}!", name);
}
Conclusion
Excellent work! You've just navigated one of the most important distinctions in Rust. Getting comfortable with String and &str will make the rest of your journey much smoother.
Key Takeaways:
Stringis an owned, heap-allocated, and growable text buffer. Use it when you need to create or modify string data.&stris an immutable, borrowed "view" into string data. It's lightweight and efficient for passing to functions that only need to read the data.- To convert from
&strtoString, use.to_string()orString::from(). This allocates new memory. - To get a
&strfrom aString, simply take a reference with&. This is cheap and does not allocate memory.
You might be wondering why Rust makes this distinction. It's all related to Rust's core feature for memory safety: ownership. The concepts of "owning" data (String) and "borrowing" a view of it (&str) are central to how Rust prevents common bugs like memory leaks or dangling pointers.
In our next lesson, we will dive headfirst into the ownership system. You'll see how it governs not just strings, but all data in a Rust program.