#241 Jul 5, 2026

241. slice::split_first — Peel the Head Off a Slice, Keep the Tail, No Indexing

slice[0] panics on an empty slice, and &slice[1..] is a second chance to get the bounds wrong. split_first hands you the head and the tail together — or None if there’s nothing there — so the empty case is a pattern, not a panic.

The manual head-and-tail

Reach for the first element and the rest, and you write two indexing operations that both assume the slice is non-empty:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn describe(items: &[&str]) -> String {
    if items.is_empty() {
        return "nothing".to_string();
    }
    let first = items[0];      // panics if you forget the guard above
    let rest = &items[1..];    // and so does this
    format!("{first} plus {} more", rest.len())
}

assert_eq!(describe(&["a", "b", "c"]), "a plus 2 more");

The bounds check is real, but it’s on you to remember the is_empty guard. Drop it and an empty slice panics at runtime.

split_first gives you both, safely

1
2
3
4
5
6
7
8
9
fn describe(items: &[&str]) -> String {
    match items.split_first() {
        Some((first, rest)) => format!("{first} plus {} more", rest.len()),
        None => "nothing".to_string(),
    }
}

assert_eq!(describe(&["a", "b", "c"]), "a plus 2 more");
assert_eq!(describe(&[]), "nothing"); // no panic — the None arm handles it

split_first returns Option<(&T, &[T])>: the first element and a slice of everything after it, or None when the slice is empty. The empty case can’t be forgotten — it’s a variant you have to match.

split_last peels from the other end

1
2
3
4
5
6
let path = ["usr", "local", "bin"];

if let Some((last, parents)) = path.split_last() {
    assert_eq!(*last, "bin");
    assert_eq!(parents, &["usr", "local"]);
}

Same shape, mirrored: the last element plus everything before it.

Where it shines: recursion without index math

Because the tail is just another slice, split_first makes structural recursion clean — the base case is None, and there’s no i + 1 to fumble:

1
2
3
4
5
6
7
8
9
fn sum(slice: &[i32]) -> i32 {
    match slice.split_first() {
        Some((head, tail)) => head + sum(tail),
        None => 0,
    }
}

assert_eq!(sum(&[1, 2, 3, 4]), 10);
assert_eq!(sum(&[]), 0);

Need to mutate as you walk? split_first_mut and split_last_mut return (&mut T, &mut [T]), so you can edit the head and recurse into the tail without a borrow fight. Whenever you catch yourself pairing slice[0] with &slice[1..], this is the method that folds both — and the empty check — into one.

← Previous 240. to_le_bytes / from_le_bytes — Serialize Integers Without unsafe or Bit-Shifting Next → 242. slice::binary_search_by_key — Find a Record by One Field, No Hand-Written Comparator