In the previous chapter, we saw how references work in Rust when we do not directly want to transfer ownership. In this chapter, we will see how we can use slices to reference a contiguous sequence of elements in a collection instead of the whole collection.

Slices

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice is a kind of reference, so it does not have ownership.

Why are slices useful?

Let’s take the example from the Rust documentation. We need to write a function that takes a string, and returns the first word separated by a space or the whole word if space is not found. The definition of the function would take in a string reference as we do not want to take ownership, but what should be the return type?

It could be the whole string, but that would mean that we are taking ownership of the string. We could return an index of the location where we see a space or the index of the last character depending on whether we could find a space. Let’s try the second approach and create a function.

fn get_first_word(s: &String) -> usize {
    let bytes = s.as_bytes(); // convert string to array of bytes

    for (i, &item) in bytes.iter().enumerate() { // iterate over the array of bytes by index
        if item == b' ' { // match the byte with the space character
            return i; // return index of the space character, function ends if found
        }
    }

    s.len() // return the length, i.e., index of the last character of string, function ends
}

Even though our implementation is correct and would work, we still have one problem. The index we derive from the function is only valid as long as the input word itself does not change. What if the string passed as reference to s changes in value or is dropped altogether? Then the index we return would no longer be valid and would point to a different value. This can be seen in the following example.

fn main() {
    let mut some_string = String::from("Hey there!"); // create a variable
    
    let first_word = get_first_word(&some_string); // returns 3, index of space character
    
    some_string.clear(); // string is cleared, but first_word variable still contains the index
}

We can see from the above example that even if the input attribute to the function is cleared, the value that the function holds is not cleared, but becomes invalid in comparison to the changed string. This is where slices come in.

String Slices

String slices are references to parts of string that we can create. It can be created as follows:

let s = String::from("Hello World!");

let first_slice = &s[0..5]; // only takes the first 5 characters
let second_slice = &s[6..11]; // takes the last 5 characters, the word 'World'

Internally, a slice stores the start index and the length of the slice to store defined by the last index.

Using the above, let’s try to rewrite our initial function of getting the first word of a string. String slice references are represented as &str in Rust.

fn get_first_word(s: &String) -> &str {
    let bytes = s.as_bytes(); // convert string to array of bytes
    
    for (i, &item) in bytes.iter().enumerate() { // iterate over the array of bytes by index
        if item == b' ' { // match the byte with the space character
            return &s[0..i]; // return the slice of the string from 0 to index of space character
        }
    }
    
    &s[..];
}

Even though much of the logic of the code remains the same to our initial implementation of the get_first_word function, we now return a string slice which will remain valid if the input string changes. Moreover, the compiler will make sure that the string slice returned is valid and does not point to an invalid index.

If we now try to drop the string after the function call, we will get an error as the string slice returned as follows:

fn main() {
    let s = String::from("Hello World!");
    
    let first_word = get_first_word(&s);
    
    s.clear(); // compiler will throw an error as the string slice returned is invalid
}

If we run the above code, we will get the following error:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

This is coherent with the borrowing references we saw in Chapter 4 - References, where we saw that if we have an immutable reference to something, we cannot also take a mutable reference. The s.clear() call will take a mutable reference to s to clear the string out, but this is not allowed in Rust. Neat!

String Literals are Slices

When we create a string literal, we are actually creating a string slice. For example, "Hello World!" is a string literal, and its type is &str. This is because string literals are stored in the binary of the program, and are therefore immutable. This is why we cannot add to a string literal, but we can add to a String type.

Other slice types

We can also create slices on array types in rust just as we created them for string types. For example, we can create a slice on an array of integers as follows:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3]; // slice of array from index 1 to 3

&assert_eq!(slice, &[2, 3]);

Summary

All the concepts we have seen in Chapter 4, 5 and 6 are very important to understand the concept of ownership, borrowing, and memory safety in rust. We will see more of these concepts in the next chapter, where we will see how to use these concepts to create a program that is memory safe and does not have any memory leaks.

Let us now look at the next chapter on Struct in Rust!