Start learning free

LESSON 200 OF 414 · STRINGS

Strings in Rust: String, Slices and chars()

Lesson Read the full explanation

The Rust string is an object that represents a sequence of characters, each identified by one index.

Creation:

To create strings in Rust, you use double quotes like this:

let greeting = "Hello";

Accessing characters:

To access the characters in a string, you use the .chars().nth() method

The following example returns the first character of a string with the index zero:

let str = "Hello";
let ch = str.chars().nth(0).unwrap();
// ch is 'H'

Getting the length:

We can get the length of a string in bytes using the .len() method (for plain ASCII text this is the number of characters, and .chars().count() counts characters in general).

To access the last character of the string, you use the .chars().last() method:

let str = "Hello";
let ch = str.chars().last().unwrap();
// ch is 'o'

Concatenating strings via + operator:

To concatenate two or more strings, you use the format! macro or the + operator:

let name = "John";
let str = format!("Hello {}", name);

println!("{}", str); // prints "Hello John"

If you want to assemble a string piece by piece, you can use the push_str() method:

let mut greet = "Welcome".to_string();
let name = "John";
greet.push_str(" to AlgoCademy, ");
greet.push_str(name);

println!("{}", greet); // prints "Welcome to AlgoCademy, John"

String slicing

We often want to get a substring of a string. For this, we use a range slice.

&str[startIndex..endIndex] returns the substring starting from startIndex and ending just before endIndex:

let str = "JavaScript";
let substr = &str[2..6];

println!("{}", substr); // prints "vaSc"

The startIndex is a zero-based index at which the slice starts extraction.

The endIndex parameter specifies where to stop (exclusive). The number of characters extracted equals endIndex - startIndex.

If you omit the end index, the slice extracts to the end of the string:

let str = "JavaScript";
let substr = &str[4..];

println!("{}", substr); // prints "Script"

Iterating through the characters:

We can iterate through the characters of a string using a for loop and the .chars() method like this:

let my_str = "Andy";
for c in my_str.chars() {
    println!("{}", c);
}

// This will print the characters 'A', 'n', 'd' and 'y' on different lines

We can also iterate through the elements of a string using indices and a for loop like this:

let my_str = "Andy";
for (i, c) in my_str.chars().enumerate() {
    println!("{}", c);
}

// This will print the characters 'A', 'n', 'd' and 'y' on different lines

Both ways take O(n) time, where n is the length of the string we iterate on.


Assignment
Follow the Coding Tutorial and let's play with some strings.


Hint
Look at the examples above if you get stuck.


STEP 1 OF 5
    Ctrl + Enter to check

    RUST · STRINGS

    Strings in Rust, explained

    What this lesson covers

    Rust has a growable, owned string type, String, and a borrowed view of text, &str. A literal in double quotes, such as "Hello", is a &str. Text is stored as UTF-8 bytes, which is why Rust handles characters, lengths and positions differently from languages where a string is an array of characters.

    The exercise turns "Java" into "TypeScript" using the main string tools: push_str, slicing, format! and chars() with collect().

    Creating strings

    let greeting = "Hello";                    // &str, can't grow
    let mut lang = "Java".to_string();         // String, can grow
    let other = String::from("Rust");          // also a String
    

    A &str is read-only. To get text you can change, make a String with to_string() or String::from, and declare the variable with mut.

    Reading characters

    Writing s[0] on a string doesn't compile in Rust. Ask the character iterator instead:

    let s = "Hello";
    let first = s.chars().nth(0).unwrap();  // 'H'
    let last = s.chars().last().unwrap();   // 'o'
    

    Both nth and last return an Option<char>, because the string might be too short or empty. unwrap() takes the character out and panics on None. For an empty string, "".chars().last() is None.

    The len() method returns the length in bytes. For plain ASCII text, such as the words in this lesson, bytes and characters are the same, but a character like é takes two bytes, so "héllo".len() is 6 while "héllo".chars().count() is 5.

    Joining strings

    format! builds a new String from pieces, with each {} replaced by the next argument. push_str appends to a String in place:

    let name = "John";
    let str = format!("Hello {}", name);       // "Hello John"
    
    let mut greet = "Welcome".to_string();
    greet.push_str(" to the course, ");
    greet.push_str(name);                      // "Welcome to the course, John"
    

    Slicing

    A range inside square brackets takes part of a string. &s[start..end] starts at start and stops before end, and leaving the end out runs to the end of the string:

    let s = "JavaScript";
    println!("{}", &s[2..6]); // vaSc
    println!("{}", &s[4..]);  // Script
    

    The leading & matters. The slice is a borrowed view into the original text, with no copy. The numbers are byte positions. Slicing panics if a number is past the end of the string or lands inside a multi-byte character. With ASCII text neither problem comes up as long as the range is inside the string.

    Looping over characters

    let my_str = "Andy";
    for c in my_str.chars() {
        println!("{}", c);
    }
    for (i, c) in my_str.chars().enumerate() {
        println!("{} {}", i, c);
    }
    

    The first loop prints A, n, d and y on separate lines. The second also gives each character its position: 0 A, 1 n, and so on. If the loop body doesn't use i, the compiler warns about an unused variable, but the code still runs. Both loops take O(n) time for a string of n characters.

    The exercise, step by step

    1. Make an owned, mutable string. "Java".to_string() creates a String from the literal, and let mut lang allows changes.
    2. Append the rest of the word. lang.push_str("script") changes lang in place so that it holds "Javascript".
    3. Take a slice. &lang[4..] is the text from position 4 to the end, "script". It borrows from lang.
    4. Join with format!. format!("Type{}", substring) produces the new String "Typescript". This variable is also mut, because the last step reassigns it.
    5. Replace a character. Since a String can't be edited by position, collect its characters into a Vec<char>, set the element at index 4 to 'S', and collect the characters back into a String. The result is "TypeScript".

    The full solution

    let mut lang = "Java".to_string();
    lang.push_str("script");
    let substring = &lang[4..];
    let mut new_lang = format!("Type{}", substring);
    let mut characters: Vec<char> = new_lang.chars().collect();
    characters[4] = 'S';
    new_lang = characters.iter().collect();
    

    After these lines, substring is "script" and new_lang is "TypeScript".

    • collect() needs a target type. The annotation Vec<char> on characters tells the first collect() what to build. For the second, the target is new_lang, which is already a String, so the same call builds a String this time.
    • iter() yields references. characters.iter() produces &char values, and String can be built from those as well as from plain char values.
    • Single quotes mean char. 'S' is a single character and "S" is a string. Assigning "S" to a Vec<char> element is a type error.
    • The slice borrows lang. While substring is in use, lang must not be changed. Here substring is last used inside format!, so nothing conflicts.

    Common mistakes

    • Indexing a string. lang[4] is a compile error. Use chars().nth(4), or a slice such as &lang[4..5].
    • Forgetting mut. push_str on a variable declared without mut doesn't compile.
    • Calling push_str on a literal. A &str can't grow. Convert it to a String first.
    • Dropping the & in a slice. lang[4..] alone has no known size, so the compiler rejects it. Write &lang[4..].
    • Off-by-one in a range. The end index is excluded, so &s[2..6] gives four characters.
    • Using byte positions with non-ASCII text. A slice that cuts a multi-byte character in half panics.

    Key takeaways

    • String is owned and growable, and &str is a borrowed view.
    • Read characters with chars(), nth and last, which return Option.
    • Build text with push_str and format!.
    • Slice ranges use byte positions and exclude the end index.
    • To change a character, collect into a Vec<char>, edit it and collect back.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free