Step 1 of 6
String slices
You often need part of a string: the first word of a command, the domain of an email address, the date at the start of a log line. Copying that part into a new String would work, but it costs an allocation every time. A slice lets you refer to part of a string without copying anything.
&s[start..end] is a string slice: a reference to the bytes of s from index start up to, but not including, end. Its type is &str.
fn main() {
let line = String::from("2024-05-17 ERROR disk full");
let date = &line[0..10];
let level = &line[11..16];
let message = &line[17..];
println!("date: {}", date);
println!("level: {}", level);
println!("message: {}", message);
}
date: 2024-05-17
level: ERROR
message: disk full
A slice is a reference made of two numbers: a pointer to its first byte and a length. (It's sometimes called a fat pointer, because it's twice the size of a plain reference.) It points straight into the String's heap text:
0 1 2
01234567890123456789012345
line: String ┌────────────┐
│ ptr ───────┼───────► 2024-05-17 ERROR disk full (heap)
│ len 26 │ ▲ ▲
│ cap 26 │ │ │
└────────────┘ │ │
date: &str ┌────────────┐ │ │
│ ptr ───────┼──────────┘ │
│ len 10 │ │
└────────────┘ │
level: &str ┌────────────┐ │
│ ptr ───────┼─────────────────────┘
│ len 5 │
└────────────┘
Nothing was copied: date and level borrow from line, with all the borrowing rules you know. While they're in use, line can't be changed or moved.
Range shorthand
| You write | You get |
|---|---|
&s[2..5] |
bytes 2, 3 and 4 |
&s[..5] |
from the start up to (not including) 5 |
&s[6..] |
from byte 6 to the end |
&s[..] |
the whole string |
&s[2..=4] |
bytes 2 to 4, including 4 |
It's the same range syntax as in for loops.
Indexes count bytes, not characters
Slice indexes are byte positions. For plain ASCII text (English letters, digits, common punctuation), one character is one byte, so it's what you'd expect. But characters like é, ü or 🦀 take 2 to 4 bytes in UTF-8, and a slice must start and end on a character boundary. If it doesn't, the program panics:
fn main() {
let word = String::from("héllo");
println!("{} bytes", word.len());
println!("{}", &word[0..2]);
}
6 bytes
The panic message explains it: end byte index 2 is not a char boundary; it is inside 'é' (bytes 1..3 of string). A slice past the end panics too (&line[0..50] on a 26-byte string), just like an array index out of bounds.
This is also why s[0] doesn't compile for strings: error[E0277]: the type `str` cannot be indexed by `{integer}` . Byte 0 might be only part of a character. The strings module shows how to walk through characters properly.
Common mistakes
- Off by one:
endis exclusive.&s[0..4]is 4 bytes: indexes 0, 1, 2 and 3. - Slicing user text at fixed byte positions. It works for ASCII but panics on accents or emoji.
- Expecting a slice to be a
String.&s[1..3]is a&str. If you need an owned copy, call.to_string()on it. Assigning it to aStringvariable directly giveserror[E0308]: mismatched types(expected `String`, found `&str`).
Your task
Fill in the two blanks with slice expressions (&email[...]) so the program splits the email address into the user name (bytes 0 to 5) and the domain (everything after the @ at index 6). It should print:
user: ferris
domain: rust-lang.org
Challenge 2
Each log entry starts with an 8-character time HH:MM:SS, then a space, then the message. Read one entry from the input and, using string slices, print its hour, its minute and its message. For the input 09:41:07 user ferris logged in it prints:
hour: 09
minute: 41
message: user ferris logged in
Challenge 3
Write mask_card, which hides all but the last 4 digits of a card number with *: "4111111111111111" becomes "************1111". If the number has 4 digits or fewer, return it unchanged. The function creates new text, so it returns an owned String.
number.len().saturating_sub(4) gives the number of digits to hide without going below zero.