RustArena

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

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.

Next: &str: the type for text parameters