String slicing with &s[0..2] counts bytes, not characters — and Rust strings are UTF-8, where a single character like é can take 2 bytes. A byte index that looked like "the first two characters" can land in the middle of one of them. Rust refuses to produce a slice that would split a character in half; it panics rather than hand back corrupted text.
fn main() {
let s = String::from("héllo");
let slice = &s[0..2];
println!("{}", slice);
}
// panicked at src/main.rs:3:19:
// end byte index 2 is not a char boundary; it is inside 'é' (bytes 1..3) of `héllo`A reference, written &T, lets code use a value without taking ownership of it — "borrowing" it temporarily. Rust’s borrowing rule, enforced by the compiler’s borrow checker: at any given time, a value can have either exactly one mutable reference (&mut T) or any number of immutable references (&T), but never both simultaneously. A slice (&[T], or &str for strings) is a reference to a contiguous portion of a collection, without owning or copying the underlying data.
Borrowing with & instead of taking ownership
Passing &value to a function lets it read the value without taking ownership — the caller keeps using the original afterward, unlike passing the value itself, which would move it.
fn calculate_length(s: &String) -> usize { // borrows, doesn't own
s.len()
}
let s1 = String::from("hello");
let len = calculate_length(&s1); // pass a reference
println!("{} is {} chars", s1, len); // s1 still usable here!The one rule: one mutable XOR many immutable
This single rule is what Rust checks at compile time to guarantee no data races: you cannot have a mutable reference alive at the same time as any other reference to the same data.
let mut s = String::from("hello");
let r1 = &s; // OK: immutable borrow
let r2 = &s; // OK: another immutable borrow, allowed
println!("{} {}", r1, r2);
let r3 = &mut s; // OK now that r1, r2 are no longer used
r3.push_str(" world");
// let bad = &s; // if r3 were still in use here: COMPILE ERRORSlices: a view into part of a collection
A slice references a contiguous range without copying it — &str (a string slice) is the most common example, and &[T] works the same way for any collection.
let s = String::from("hello world");
let hello = &s[0..5]; // slice: "hello", no copy of the data
let world = &s[6..11]; // slice: "world"
let numbers = [1, 2, 3, 4, 5];
let middle = &numbers[1..4]; // &[2, 3, 4]Byte-range slicing like &s[0..2] works safely whenever the bounds line up with real character edges — which they always do for ASCII, and that's exactly why the bug above is easy to miss in testing until a non-ASCII name or word shows up. The safe way to take "the first N characters" is to count characters instead of bytes, with .chars():
fn main() {
let s = String::from("héllo");
let slice: String = s.chars().take(2).collect();
println!("{}", slice);
}
// hé