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:

  1. Concept Explanation: We’ll define String and &str, explain their differences in terms of memory management (stack vs. heap), ownership, and mutability.
  2. Live Coding Demonstration: We’ll write code to demonstrate how to create, use, and convert between String and &str. We’ll also show common methods and operations.
  3. 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 String and &str.
  • Explain how String and &str are stored in memory.
  • Create String and &str values.
  • Convert between String and &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 String owns the data it points to. When the String goes 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 empty String)
  • &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 String using &mut str, but this is still a borrow, and the underlying data must be owned by a String.)
    • Fixed Size (at Compile Time): A &str itself 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 &str is 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 'static lifetime – 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)

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 String and &str values.
  • Mutability: Only String can be modified directly.
  • Methods: Many string methods work on both String and &str thanks to deref coercion.
  • Conversions: We demonstrate how to convert between the two types using borrowing (&), to_string(), and into().
  • Function Arguments: Deref coercion allows us to pass &String where &str is expected.
  • String Concatenation: We show two ways to concatenate strings using + operator and format!.

5. Tasks

  1. String Creation: Create a String variable using String::from(). Create a &str variable using a string literal. Create an empty String using String::new().
  2. Slicing: Create a String variable. Create a &str slice that refers to a portion of that String.
  3. Conversion: Create a &str variable. Convert it to a String. Create a String variable. Create a &str that borrows from it.
  4. Function with &str: Write a function that takes a &str as an argument and returns a new String that is the uppercase version of the input.
  5. Function with String: Write a function that takes a String as an argument, modifies it (e.g., by appending something), and then returns the modified String.
  6. Concatenation Practice: Use both the + operator (and borrowing) and the format! macro to concatenate several strings together.
  7. Fix the Errors: Uncomment error line and fix all of errors.
  8. Deref Coercion Exploration: Write a function that takes a &str as 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.
  9. Mutable String Slice: Create a mutable String variable. Create a mutable string slice (&mut str) from it. Modify the string through the mutable slice. Print the original String to confirm the change.