[Rust Series 7] Handling Slices and Strings Safely

한국어 버전

Once you understand the borrowing rules from part 6, it’s time to see them applied to real data. Strings and slices are Rust’s go-to structures for "use a portion without cloning the whole value." We’ll compare String to &str, slice a string safely, and see why the compiler blocks suspicious slices.

Just remember the exact same borrowing rules: a slice is still a reference, so it follows the same limits.

String vs &str at a Glance

  1. String: an owned, growable heap string. You can mutate it, but ownership moves on transfer.
  2. &str: a string slice, an immutable reference to existing UTF-8 data. Literals like "hello" are &'static str.

The key difference is "who owns the bytes." String holds them, &str borrows them. Keep that in mind and every slice rule feels natural.

Tracking Study Goals with Strings

Let’s extend the study-tracker example. This time we collect goals in a single string and slice out just the first one.

fn main() {
    let mut goals = String::from("ownership,borrowing,strings");
    let first = first_goal(&goals);
    println!("First goal: {}", first);

    goals.push_str(",structs");
    println!("Updated goals: {}", goals);
}

fn first_goal(goal_list: &String) -> &str {
    let bytes = goal_list.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b',' {
            return &goal_list[0..i];
        }
    }

    &goal_list[..]
}

The slice syntax &goal_list[start..end] is Rust’s built-in way to borrow part of a string. as_bytes plus b',' means we inspect it at the byte level. Because Rust strings are UTF-8, you must respect character boundaries when slicing. This example is safe because the input stays in ASCII.

Not every string is that simple. Some characters take multiple bytes, so reach for chars() when you need character-aware logic.

fn main() {
    let text = "가";
    println!("Character count: {}", text.chars().count());
}

Slice Rules and Safety Nets

Slices are references, so they obey the same borrowing limits:

  • A slice doesn’t copy data.
  • Creating a slice means you now have an immutable borrow of that range.
  • While that slice is alive, you can’t take a mutable borrow of the original.
fn main() {
    let mut label = String::from("Rust");
    let part = &label[0..2];
    label.push_str("ace"); // error: part still lives, so mutation is blocked
    println!("{}", part);
}

label.push_str might reallocate the buffer, leaving part pointing to invalid memory. Rust would rather stop you upfront. The usual fix is to use the slice, let it go out of scope, and only then mutate—or clone the slice into a new string before editing.

If you slice on the wrong boundary, you won’t get a compile error but a runtime panic. That’s why treating UTF-8 as "bytes vs characters" consciously matters.

Passing Strings and Slices into Functions

String-related APIs often accept &str so callers can pass either an owned String or a literal.

fn summarize(target: &str) {
    println!("Summary: {}", target);
}

fn main() {
    let owned = String::from("borrowing in progress");
    summarize(&owned); // String -> &str coercion
    summarize("previewing enums"); // literals are already &str
}

This pattern is everywhere: accept &str when read-only access is enough, return String only when you must own the result, and reserve &mut String for the few APIs that mutate in place.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Rust starter. Move the main code into src/main.rs, then compare cargo check and cargo run so you can read the compiler feedback beside the final output.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. Fork the starter and open src/main.rs
  2. Paste the lesson code and run cargo check plus cargo run in order
  3. Change types, values, or borrowing flow and compare the compiler feedback with the output

Rust practice here is mainly terminal-driven rather than browser-preview driven. Lessons that need multiple files or extra crates may require a bit more setup inside the starter.

Try It Yourself

  • Slice a string that contains UTF-8 emojis and observe the error you hit.
  • Write a function that takes a Vec<String> and returns Vec<usize> by measuring each item through slices.
  • Split the goals into a Vec<String> and print each entry as an &str slice.

Wrap-Up

Slices are the tool that lets you borrow just part of a value. Once you internalize the ownership split between String and &str, it’s obvious why slices are immutable references and why they can’t overlap with mutable borrows. Next up we’ll build structs and methods to see how these ideas play out inside custom types.

💬 댓글

이 글에 대한 의견을 남겨주세요