Start learning free

LESSON 60 OF 414 · STRINGS I

Accessing Characters in Rust: Slices, chars() and nth()

Lesson Read the full explanation

Each character in a string has a numbered position known as its index. We access a character by referring to its index number.

Strings use zero-based indexing, so the first character in a string has an index of 0, the second character has an index of 1, etc.

Rust can't index a string with a single integer, so we first collect its characters into a vector of char and then use bracket notation ([]) with the index after the name of the vector. (In the tutorial we'll instead slice one byte, like &fruit[0..1], which is safe for plain ASCII text.)

let message = "Hello world!";
let chars: Vec<char> = message.chars().collect();

println!("{}", chars[0]); // Output: H
println!("{}", chars[1]); // Output: e
println!("{}", chars[6]); // Output: w

Whitespaces have indices:

Note that every character inside the quotes (including whitespaces) is part of the string and has its own index. For example, in string "Hello world!", the whitespace is placed on index 5.


Assignment
Follow the Coding Tutorial and let's access some characters!


Hint
Look at the examples above if you get stuck.


STEP 1 OF 2
    Ctrl + Enter to check

    RUST · STRINGS I

    Accessing Characters in Rust, explained

    Positions in a string

    Every character in a string sits at a numbered position, counted from 0. In "Hello world!" the H is at position 0, the e at 1, and the space between the words at 5. Spaces and punctuation count like any other character.

    Many languages let you write message[0] to read a character. Rust does not. The reason is how Rust stores text: a String or &str is a sequence of bytes in UTF-8, and a single character may take one to four of them. Position 3 in bytes is not always the fourth character, so a plain index has no single obvious meaning. This code does not compile:

    let s = "Hello";
    println!("{}", s[0]);
    

    The compiler says the type `str` cannot be indexed by `{integer}` and suggests .chars().nth() or .bytes().nth(). Rust gives you several explicit ways to ask for a character instead, and each says exactly what it counts.

    Option 1: collect the characters into a vector

    chars() walks over the characters of a string, and collect() gathers them into a Vec<char>. A vector can be indexed with brackets, and the index counts characters:

    let message = "Hello world!";
    let chars: Vec<char> = message.chars().collect();
    println!("{}", chars[0]);
    println!("{}", chars[1]);
    println!("{}", chars[6]);
    
    H
    e
    w
    

    This copies the whole string into a new vector, which is worth it when you access many positions. The type annotation Vec<char> tells collect() what to build. Indexing outside the vector panics.

    Option 2: nth() on the iterator

    For a single character, message.chars().nth(4) skips four characters and returns an Option<char>. It is Some('o') for this message and None when the position is past the end, so no panic happens until you call unwrap() on a None. The cost grows with the position, because nth has to walk from the start.

    Option 3: a one-byte slice

    A range in brackets slices a string by byte positions and borrows part of it. A range a..b includes a and excludes b, so 0..1 is exactly the first byte:

    let fruit = "Avocado";
    println!("{}", &fruit[0..1]);
    println!("{}", &fruit[6..7]);
    
    A
    o
    

    The result is a &str of length one, not a char. The & is required: the slice itself, fruit[0..1], is an unsized str that can't be stored or passed around directly, so you borrow it. This works well for text where every character is one byte, which is true of plain English letters, digits and common punctuation (ASCII).

    Bytes and characters differ outside ASCII

    Take the string "héllo". The letter é takes two bytes, so len() returns 6 while chars().count() returns 5. The character at position 1 is é through chars().nth(1), which gives Some('é'). A slice that cuts through the middle of that character, such as &s[0..2], panics at run time with a message that the byte index is not a char boundary. The checked method get(1..2) returns None in that case instead of panicking.

    So pick the tool by what you are counting. Slices and len() count bytes and are fast, but only safe when you know the text is ASCII. chars() counts characters.

    The lesson's exercise

    The exercise declares fruit as the literal "Avocado" and prints two characters.

    1. The first character. The slice &fruit[0..1] covers the byte at position 0.
    2. The last character. The word has 7 bytes, so the last position is 6, and the slice &fruit[6..7] picks it out.
    let fruit = "Avocado";
    println!("{}", &fruit[0..1]);
    println!("{}", &fruit[6..7]);
    

    Both slices are safe because every letter of "Avocado" is a single byte.

    Common mistakes

    • Writing s[i]. It does not compile for strings; use a slice, chars() or as_bytes().
    • Forgetting that the end of a range is excluded. &fruit[0..1] is one character, &fruit[0..0] is empty.
    • Reading past the end. &fruit[7..8] panics because the string has only 7 bytes. The safe form fruit.get(7..8) returns None.
    • Treating a slice as a char. A slice is text; to get a char use chars().nth(i) or a vector.

    Key takeaways

    • Positions start at 0, and spaces count.
    • Rust strings can't be indexed by a single integer, because characters vary in byte length.
    • Vec<char> and chars().nth(i) count characters; &s[a..b] counts bytes.
    • Prefer the checked get or nth when a position might be out of range.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free