RustArena

Step 3 of 6

Returning slices: first_word

Suppose you need the first word of a command. Without slices, you might return the index where the word ends. But that number knows nothing about the string it came from. If the string changes later, the index is stale and silently wrong:

fn first_word_end(s: &str) -> usize {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return i;
        }
    }
    s.len()
}

fn main() {
    let mut command = String::from("move north");
    let end = first_word_end(&command);
    command.clear();                          // the string is now empty...
    println!("first word ends at {}", end);   // ...but end still says 4
}
first word ends at 4

Two new pieces here: s.as_bytes() views the text as a slice of bytes (&[u8]), and b' ' is the byte value of a space. Looking at bytes is safe for this job: a space is one byte in UTF-8, and no multi-byte character contains that byte. command.clear() empties a String.

Now return a slice instead of an index:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }
    s
}

fn main() {
    let command = String::from("move north");
    let word = first_word(&command);
    println!("command: {}", word);
    println!("single: {}", first_word("look"));
}
command: move
single: look

If there's no space, the whole string is the first word, and s is already a &str, so it's returned as is.

The returned &str borrows from the argument. Rust works this out without being told: when a function has exactly one reference parameter, Rust treats any reference it returns as borrowing from that parameter. (The lifetimes module covers the rules.) So the borrow checker ties word to command, and the stale-index bug becomes a compile error:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }
    s
}

fn main() {
    let mut command = String::from("move north");
    let word = first_word(&command);
    command.clear();
    println!("{}", word);
}
error[E0502]: cannot borrow `command` as mutable because it is also borrowed as immutable
  --> src/main.rs:14:5
   |
13 |     let word = first_word(&command);
   |                           -------- immutable borrow occurs here
14 |     command.clear();
   |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
15 |     println!("{}", word);
   |                    ---- immutable borrow later used here

clear needs a &mut borrow of command while word still borrows it: "many readers or one writer". Use the word first, or copy it with word.to_string() if you really need it after changing the string.

Cutting at an index: split_at

s.split_at(i) gives you both halves at once, as two slices: (&s[..i], &s[i..]).

fn main() {
    let code = "EU-4417";
    let (region, rest) = code.split_at(2);
    println!("region {} / number {}", region, &rest[1..]);
}
region EU / number 4417

Workplace note: functions that find something in their input return a slice of it, which costs no allocation at all. The standard library works this way everywhere (trim, split_whitespace, lines). Callers who need to keep the result after the original changes call .to_string() on it. And clippy will suggest writing the index loop above with .iter().enumerate(), which you'll learn in the iterators module; the loop with an index does the same thing.

Common mistakes

Your task

Write last_word, which returns the part of s after its last space (or the whole of s if there's no space): last_word("pick up the key") returns "key". Walk the bytes backwards with for i in (0..bytes.len()).rev().

Challenge 2

Write extension, which returns a file name's extension: the part after its last .. If there's no dot, return "" (an empty literal is a fine &str to return). Examples: "report.pdf" gives "pdf", "archive.tar.gz" gives "gz", "README" gives "".

Challenge 3

Write split_command, which splits a text-adventure command into its verb and the rest, both as slices of the input: "take rusty key" gives ("take", "rusty key"). If there's no space, the rest is "". Use split_at, or two slices.

Previous: &str: the type for text parameters Next: Array and Vec slices: &[T]