String Slices and String
1. Welcome and Icebreaker:
Welcome back!
If you could have a superpower related to text manipulation, what would it be? (Think beyond just basic editing!)
2. Outline the Session:
Here’s the plan:
- Concept Explanation: We’ll define
Stringand&str, explain their differences in terms of memory management (stack vs. heap), ownership, and mutability. - Live Coding Demonstration: We’ll write code to demonstrate how to create, use, and convert between
Stringand&str. We’ll also show common methods and operations. - Tasks: You’ll practice working with both string types, including conversions and common string manipulations.
Learning Outcomes: By the end of this session, you will be able to:
- Understand the fundamental difference between
Stringand&str. - Explain how
Stringand&strare stored in memory. - Create
Stringand&strvalues. - Convert between
Stringand&str. - Use common string methods on both types.
- Choose the appropriate string type for different situations.
Expectations: Pay close attention to the memory management aspects, as this is where the key differences lie. As always, experiment with the code!
3. Concept Explanation:
String:- Heap-allocated: The string data is stored on the heap. This allows the string to grow or shrink in size dynamically.
- Owned: A
Stringowns the data it points to. When theStringgoes out of scope, the memory on the heap is freed. - Mutable: You can modify a
String(append to it, insert characters, etc.). - Created using:
String::from("text")"text".to_string()String::new()(creates an emptyString)
&str(String Slice):- Borrowed: A string slice is a reference to a sequence of UTF-8 encoded bytes. It does not own the underlying data.
- Immutable: You cannot modify the data that a string slice points to. (You can create a mutable borrow of a
Stringusing&mut str, but this is still a borrow, and the underlying data must be owned by aString.) - Fixed Size (at Compile Time): A
&stritself has a known, fixed size at compile time because it’s essentially a pointer and a length. The data it points to can be of any size, but the reference itself is fixed. - Two Parts: A
&stris represented internally as two values:- A pointer to the beginning of the string data (in memory).
- The length of the string slice (in bytes).
- Created using:
- String literals:
"hello"(This creates a string slice with a'staticlifetime – it lives for the entire duration of the program). - Slicing a
String:&my_string[0..5] - Dereferencing a
String:&my_string(This often happens automatically due to deref coercion, which we’ll discuss below)
- String literals:
Key Differences and Analogy:
| Feature | String |
&str |
|---|---|---|
| Memory | Heap | Stack (the reference), Data can be anywhere |
| Ownership | Owned | Borrowed |
| Mutability | Mutable | Immutable |
| Size | Growable/Shrinkable | Fixed size (the reference, not necessarily data) |
Analogy: Think of a String like a handwritten notebook that you own. You can add pages, erase text, and modify it freely. A &str is like a sticky note pointing to a specific section within that notebook (or even a section of someone else’s notebook). You can read the text on the sticky note, but you can’t change the original notebook’s content through the sticky note alone.
Deref Coercion:
Rust has a feature called “deref coercion” that makes working with String and &str more convenient. It automatically converts a &String to a &str when needed. This is why you can often pass a &String to a function that expects a &str.
4. Live Coding Demonstration:
fn main() {
// --- Creating Strings ---
let s1 = String::from("Hello"); // Heap-allocated, owned
let s2 = "world".to_string(); // Also heap-allocated, owned
let s3 = String::new(); // Empty String, heap-allocated
// --- Creating String Slices ---
let slice1: &str = "literal"; // String literal (&str with 'static lifetime)
let slice2 = &s1[0..2]; // Slice of a String (&str)
let slice3 = &s2; // Deref coercion: &String -> &str
// --- Mutating Strings ---
let mut s4 = String::from("Growable");
s4.push_str(" string"); // Appending to a String
s4.push('!'); // Appending a single character
println!("s4: {}", s4);
// --- String Methods (many work on both String and &str) ---
println!("Length of s1: {}", s1.len()); // Length in bytes
println!("Contains 'ell': {}", s1.contains("ell"));
println!("Is empty: {}", s3.is_empty());
println!("slice1: {}", slice1.to_uppercase());
// --- Converting between String and &str ---
let s5 = String::from("Convert me");
let slice4: &str = &s5; // String to &str (borrowing)
let s6: String = slice4.to_string(); // &str to String (creates a new owned String)
let s7: String = slice4.into(); // &str to String (also creates a new owned String)
// --- Passing to Functions ---
print_string_slice(slice1); // Passing a string literal
print_string_slice(&s1); // Passing a &String (deref coercion)
print_string_slice(slice2); // Passing a slice
//print_string(slice1); // Error
print_string(s1); // Passing a String
//println!("{}",s1) // Error: s1 moved to print_string function
// --- String Concatenation ---
let str1 = String::from("Hello, ");
let str2 = String::from("world!");
let str3 = str1 + &str2; // Note: str1 is moved here! &str2 is borrowed.
println!("{}", str3);
//println!("{}", str1); // Error str1 moved
let str4 = String::from("Hello, ");
let str5 = String::from("world!");
let str6 = format!("{}{}",str4,str5); // format! macro, good for complex cases
println!("{}", str6);
println!("{}", str4);
println!("{}", str5);
}
fn print_string_slice(s: &str) {
println!("Received a string slice: {}", s);
}
fn print_string(s: String) {
println!("Received a string : {}", s);
}
Key Takeaways:
- Creation: We show different ways to create
Stringand&strvalues. - Mutability: Only
Stringcan be modified directly. - Methods: Many string methods work on both
Stringand&strthanks to deref coercion. - Conversions: We demonstrate how to convert between the two types using borrowing (
&),to_string(), andinto(). - Function Arguments: Deref coercion allows us to pass
&Stringwhere&stris expected. - String Concatenation: We show two ways to concatenate strings using
+operator andformat!.
5. Tasks
- String Creation: Create a
Stringvariable usingString::from(). Create a&strvariable using a string literal. Create an emptyStringusingString::new(). - Slicing: Create a
Stringvariable. Create a&strslice that refers to a portion of thatString. - Conversion: Create a
&strvariable. Convert it to aString. Create aStringvariable. Create a&strthat borrows from it. - Function with
&str: Write a function that takes a&stras an argument and returns a newStringthat is the uppercase version of the input. - Function with
String: Write a function that takes aStringas an argument, modifies it (e.g., by appending something), and then returns the modifiedString. - Concatenation Practice: Use both the
+operator (and borrowing) and theformat!macro to concatenate several strings together. - Fix the Errors: Uncomment error line and fix all of errors.
- Deref Coercion Exploration: Write a function that takes a
&stras an argument. Call this function with:- A string literal.
- A
&String. - A slice of a
String. Observe that all of these calls work without any explicit conversions, thanks to deref coercion.
- Mutable String Slice: Create a mutable
Stringvariable. Create a mutable string slice (&mut str) from it. Modify the string through the mutable slice. Print the originalStringto confirm the change.