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
- Forgetting the "no space" case. Without the final
s, the function has nothing to return when there's no space, so it doesn't compile:error[E0308]: mismatched types(expected `&str`, found `()`). - Changing the string while a slice of it is in use (E0502). Use the slice first.
- Returning a slice of a
Stringbuilt inside the function (E0515). Return theStringinstead.
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]