Working Code
You've used strings in two different ways so far. Let's see the difference:
fn main() {
// Approach 1: just double quotes
let greeting = "hello";
// Approach 2: String::from()
let mut name = String::from("Alice");
name.push_str("!");
println!("{greeting}, {name}");
}
"hello" works on its own — so why do you sometimes need String::from()?
Try It Yourself
- Try calling
push_str("!")ongreeting. What error do you get? - Change
nametolet name = "Alice";and trypush_str. What happens?
"Why?" — Two String Types Exist for a Reason
From an ownership perspective, it's clear:
| | String | &str |
| --------- | ---------------- | --------------------------------------------- |
| Analogy | A string you own | A borrowed string |
| Storage | Heap (resizable) | Read-only memory or a slice of another String |
| Mutable | Yes, if mut | No |
| Ownership | Owned | None (just a reference) |
A string literal like "hello" is type &str. It's baked into the program binary and can't be modified. You're borrowing it.
String::from("hello") copies that content to the heap and makes it yours. Since you own it, you can modify it.
Ways to Create a String
fn main() {
// Method 1: String::from()
let s1 = String::from("hello");
// Method 2: .to_string()
let s2 = "hello".to_string();
// Method 3: format! macro
let name = "Alice";
let age = 25;
let s3 = format!("{name} is {age} years old");
println!("{s1}, {s2}, {s3}");
}
All three produce a String. format! is handy for combining multiple values.
Modifying a String
fn main() {
let mut s = String::from("hel");
s.push_str("lo"); // append a string slice
s.push('!'); // append a single character
println!("{s}"); // "hello!"
println!("Length: {}", s.len()); // byte count
}
push_str() takes a &str and appends it. push() adds a single char.
String Slices — Borrowing a Portion
&str can also point to part of a string:
fn main() {
let sentence = String::from("hello world");
let hello = &sentence[0..5]; // "hello"
let world = &sentence[6..11]; // "world"
println!("{hello}, {world}");
}
&sentence[0..5] borrows bytes 0 through 4 from sentence. It doesn't create a new String — it references a portion of the original.
Function Parameters — &str Is More Flexible
When accepting strings in a function, prefer &str:
fn greet(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let owned = String::from("Alice");
let literal = "Bob";
greet(&owned); // String passed as &str — works!
greet(literal); // &str works too, of course!
}
Using &str as a parameter accepts both String and string literals. If you used &String, you could only accept String. That's why &str is the more flexible choice.
Learn More: Converting Between String and &str
Here's how to go back and forth between String and &str:
fn main() {
// &str -> String
let s: String = "hello".to_string();
let s2: String = String::from("hello");
// String -> &str
let borrowed: &str = &s; // automatic coercion
let slice: &str = s.as_str(); // explicit conversion
println!("{borrowed}, {slice}");
}
Converting String to &str is just borrowing — zero cost. Converting &str to String copies data to the heap — there is a cost.
Exercise 1. Complete the function below. It takes a name and age, then returns a greeting string.
fn make_greeting(name: &str, age: i32) -> String {
// Return "Hello, {name}! You're {age} years old!"
// Hint: use the format! macro
}
fn main() {
let msg = make_greeting("Alice", 25);
println!("{msg}");
}
Exercise 2. Fix the error below. Try to solve it without using clone().
fn print_length(s: String) {
println!("Length: {}", s.len());
}
fn main() {
let word = String::from("Rust");
print_length(word);
println!("Word: {word}"); // error!
}
Hint: How can you stop the function from taking ownership? Change the parameter type.
Q1. What is the biggest difference between String and &str?
- A)
Stringstores numbers;&strstores characters - B)
Stringis owned and mutable;&stris a borrowed reference - C)
Stringis slow;&stris fast - D) No difference — they are the same type
Q2. Why is &str a good choice for function parameters?
- A) It's faster
- B) It can accept both
Stringand&str, making it flexible - C) It never produces errors
- D) It lets you modify the string
Q3. What does this code print?
fn main() {
let s = String::from("hello world");
let word = &s[0..5];
println!("{word}");
}
- A) "hello world"
- B) "hello"
- C) "h"
- D) Compile error