Slices in Rust

Slices in Rust

Let's learn more about this built-in type

The Slice type in Rust

Rust has a built-in type called Slice that is used to reference a contiguous sequence of elements in a collection. Slices are a very important concept in Rust, and they are used extensively in the standard library. In this lesson, we will explore the Slice type and how it is used in Rust.

If you prefer a video version

All the code is available on GitHub (link available in the video description)

What is a Slice?

A slice is a reference to a contiguous sequence of elements in a collection.

Slices are used to reference a portion of a collection, and they are used extensively in the standard library.

Slices are a very important concept in Rust, and they are used extensively in the standard library. In this lesson, we will explore the Slice type and how it is used in Rust.

A first example

Let's see a simple example of a slice. We will use an array:

fn main() {
    let a = ["a", "b", "c", "d", "e"];
    let slice = &a[1..3];
    println!("{:?}", slice);
}

In this example, we have an array a with five elements. We then create a slice slice that references the second and third elements of the array.

When we run the program, we get the following output:

["b", "c"]

The Range 1..3 is used to create the slice. The first number is the starting index, and the second number is the ending index.

The ending index is exclusive, which means that the element at the ending index is not included in the slice.

A second example

Let's try a slice of a vector of integers:

fn main() {
    let v = vec![10, 20, 30, 40, 50];
    let slice = &v[3..4];
    println!("{:?}", slice);
}

In this example, we have a vector v with five elements. We then create a slice slice that references the fourth element of the vector.

When we run the program, we get the following output:

[40]

Third Example

Let's try a slice of a string:

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];
    println!("{:?}", hello);
    println!("{:?}", world);
}

In this example, we have a string s with the value "hello world". We then create two slices, hello and world, that reference the first five and last five characters of the string, respectively.

When we run the program, we get the following output:

"hello"
"world"

Slices in Rust - Rust programming tutorial

Range shortcuts for slices

There are some shortcuts for creating slices. For example, if you want to start at index 0, you can omit the first number:

let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];

Will have as an output:

"he"

Also, if you want to go to the end of the string, you can omit the second number:

let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];

Will have as an output:

"lo"

You can also get the entire string by using the .. syntax:

let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];

Will have as an output:

"hello"

Exercise (without Slices)

Let's write a program that takes a string and returns the first word in the string.

First, let'simplement a solution without using slices.

Here is the code:

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    println!("the first word is: {}", word);
}


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

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

    s.len()
}

Explanation:

  • The as_bytes method returns a slice of the string's bytes.
  • The iter method returns an iterator over the slice.
  • The enumerate method returns an iterator that yields the index and the value of each element in the slice.
  • The b' ' syntax is used to create a byte literal.
  • The s.len() method returns the length of the string.

We get this output:

the first word is: 5

In this case, 5 is the index of the first space in the string, which is the end of the first word.

But there is a problem with this code. If we try to clear the string s after calling the first_word function, we will NOT get a compile error. The variable word will still have the value 5, even though the string s has been cleared.

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s); // word will get the value 5
    println!("the s is: {}", s);
    println!("the first word is: {}", word);

    s.clear(); // this empties the String, making it equal to ""
    println!("the s is: {}", s);
    println!("the first word is: {}", word);    
}

The output will be:

the s is: hello world
the first word is: 5
the s is:
the first word is: 5

The word variable still has the value 5, but the string s has been cleared.

Let's see how we can fix this using slices.

Exercise (using slices)

Let's go back to our programming example.

Using slices, we can rewrite the first_word function like this:

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

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

    &s[..]
}

Now, the first_word function returns a string slice, which is a reference to part of the original string.

If now we have somerthing like this:

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s); // word will get the value 5
    println!("the s is: {}", s);
    println!("the first word is: {}", word);

    s.clear(); // this empties the String, making it equal to ""
    println!("the s is: {}", s);
    println!("the first word is: {}", word);    
}

We will get a compile error, because we are trying to use word after s has been cleared.

String literals as Slices

Recall that we talked about string literals being stored inside the binary.

Now that we know about slices, we can understand string literals.

Update the code in the main function to use a string literal below the s variable (you can keep the lines that use s as well). :

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s); // word will get the value 5
    println!("the s is: {}", s);
    println!("the first word is: {}", word);

    let s2 = "hello world";
    let word2 = first_word(&s2); // word will get the value 5
    println!("the s is: {}", s2);
    println!("the first word is: {}", word2);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

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

    &s[..]
}

The type of s is &str, which is a slice of a string. This means that s is a reference to a contiguous sequence of characters in memory.

As a final note, the type of s2 is &str, which is a slice of a string. This means that s2 is a reference to a contiguous sequence of characters in memory.

So we can remove the &s2 from the first_word function and the code will still work.

Conclusion

In this lesson, we explored the Slice type in Rust. We learned that a slice is a reference to a contiguous sequence of elements in a collection.

We also learned how to create slices using the Range type and how to use slices to reference parts of a string.

In the next lesson, we will see the Struct type in Rust.

If you prefer a video version

All the code is available on GitHub (link available in the video description)

Did you find this article valuable?

Support Francesco Ciulla by becoming a sponsor. Any amount is appreciated!