While manipulating strings, it is a common requirement to slice parts of the string. The Slice Type in Rust makes it easy and error-free to slice strings.

The Slice Type is particularly useful because it follows the principles of Ownership and Borrowing in Rust.

1 – Why do we need Slice Type?

To understand the need of the Slice Type, let us look at a simple requirement.

We would like to create a function that takes a string and returns the first word it finds in that string. The boundary of the first word is the first space we find within the string. If the function doesn’t find a space, the whole string must be one word.

Below is a very basic implementation of such a function.

fn main() {
    let s = String::from("Slice Demo");
    let word_index = get_first_word(&s);
    println!("The first word ends at index {}", word_index);
}

fn get_first_word(input: &String) -> usize {
    let bytes = input.as_bytes();

    for (index, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return index;
        }
    }

    input.len()
}

As you can see, the function get_first_word() accepts a String reference. It returns the index value where the first word ends. If interested, you can read this post on Functions in Rust.

To find the index, we convert the string to bytes and then iterate over the same. The enumerate() function basically returns a tuple comprising of the index and the item. If the current item is a space, we return the index value. If we never find a space in the string, we return the length of the string. This means that the entire string is one word.

While the above implementation is pretty simple, it has one problem. See below change in the main() function.

fn main() {
    let mut s = String::from("Slice Demo");
    let word_index = get_first_word(&s);
    s.clear();
    println!("The first word ends at index {}", word_index);
}

We changed the string to mutable using mut keyword. Also, after making the call to get_first_word() function, we cleared the String using s.clear().

Basically, this disconnects the word_index from the actual string. Even though the string s is now a blank string, the word_index still retains the value 5. If we have to write an error-free code, we need to find a way to keep the word_index in sync with the actual string.

This is where string slices come into the picture.

2 – Using the Rust String Slice

A string slice in Rust is a reference to a part of a String.

See below example:

let mut s = String::from("Slice Demo");
let first_word = &s[0..5];
let second_word = &s[6..10];
println!("First Word: {}", first_word);
println!("Second Word: {}", second_word);

Slices are created using a range within the brackets. The first argument is the starting index and the second argument is the ending index. Basically, the starting_index is the first position in the slice and the ending_index is one more than the last position in the slice.

Internally, the slice data structure stores the starting position and the length of the slice. The length is nothing but the ending_index minus the starting_index. For example, the length of the second_word is 4 (10 – 6).

The range syntax allows for a few variations.

For example, if our range starts from 0, we can drop the value before the two periods.

let mut s = String::from("Slice Demo");
let first_word = &s[..5];

Also, if our slice includes the last byte of the String, we can drop the trailing number.

let mut s = String::from("Slice Demo");
let second_word = &s[6..];

We can also drop both values to take a slice of the entire string.

let mut s = String::from("Slice Demo");
let full_world = &s[..];

We can now tweak our original function to return first word using the Rust slice type. See below.

fn get_first_word(input: &String) -> &str {
    let bytes = input.as_bytes();

    for (index, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &input[0..index];
        }
    }

    return &input[..]
}

Instead of returning the index, we are now returning string slices. This removes the chances of introducing bugs in our program. If the string changes down the line, our get_first_word() function will always give correct results.

3 – Using String Slices as Parameters

There is one more adjustment that we can do. Instead of passing a String, we can pass a string slice as input to the get_first_word() function.

fn main() {
    let mut s = String::from("Slice Demo");
    let word = get_first_word(&s[..]);
    println!("First Word Is:{}", word);
    let another_word = get_first_word(&s);
    println!("First Word Is:{}", another_word);
}

fn get_first_word(input: &str) -> &str {
    let bytes = input.as_bytes();

    for (index, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &input[0..index];
        }
    }

    return &input[..]
}

Here, we have changed our function’s signature to accept string slices instead of the String. This makes our program more flexible. If we have a String, we can pass a slice of the String or even a reference to the String.

We can also directly pass string literals in this approach. This is because string literals are string slices.

Conclusion

The Rust Slice Type makes it extremely convenient to work with parts of the string. It also safeguards against issues with regards to ownership, borrowing and memory safety in a Rust program.

If you have any comments or queries about this post, please feel free to mention them in the comments section below.

Categories: BlogRust

Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *