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
- String: an owned, growable heap string. You can mutate it, but ownership moves on transfer.
- &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.
💬 댓글
이 글에 대한 의견을 남겨주세요