DEV Community

Cover image for [Rust Guide] 4.5. Slice
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 4.5. Slice

4.5.0 Before We Begin

This is the last article in Chapter 4, so let’s also take the opportunity to summarize this chapter:

The concepts of ownership, borrowing, and slices ensure memory safety in Rust programs at compile time. Rust allows programmers to control memory usage in the same way as other systems programming languages, but letting the owner of the data automatically clean it up when it goes out of scope means you do not need to write and debug extra code to gain that control.

After reading this article, I believe you will sincerely marvel at how magical and advanced Rust’s ownership mechanism really is.

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

4.5.1 Slice Features

  • 1. Type and structure

    • Slice types are represented as &[T] or &mut [T], where T is the type of the elements in the slice.
    • Immutable slices: &[T], which only allow read operations.
    • Mutable slices: &mut [T], which allow modification.
  • 2. Do not own data

    • A slice is essentially a reference to the underlying data, so it does not own the data.
    • A slice’s lifetime is the same as the underlying data. When the underlying data is destroyed, the slice becomes invalid too.

4.5.2 String Slices

Take a problem as an example:
Write a function that accepts a string as an argument, and returns the first word it finds in that string. If the function does not find any spaces, the entire string is returned.

fn main() {
    let s = String::from("Hello world");
    let word_index = first_word(&s);
    println!("{}", word_index);
}
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()
}
Enter fullscreen mode Exit fullscreen mode
  • Because you need to iterate over String element by element and check whether each value is a space, you use the as_bytes method to convert String into a byte array.
  • We will talk about iterators later. For now, all you need to know is that iter is a method used to retrieve each element in a collection one by one. enumerate is a tool that adds an index to each element on top of iter and returns the result as a tuple. The first element of the returned tuple is the index, and the second element is a reference to that element.

The program compiles successfully, and the output is 5. That is the index of the space after Hello.

We now have a way to find the index of the end of the first word in a string, but there is a problem. We return a usize ourselves, but it is only a number that has meaning in the context of &String. In other words, because it is a value different from String, there is no guarantee that it will still be valid in the future.

For example, for some reason the code writes s.clear(); after calling first_word to clear s. At that point, the word_index variable no longer means anything. Put another way, the Rust compiler cannot detect the error where the code uses s.clear() while word_index still exists. If you later use word_index to print a character in your code, an error will obviously occur.

This kind of API design requires constantly paying attention to the validity of word_index, and ensuring the synchronization between this index and the String variable s. Unfortunately, this kind of work is often quite tedious and very error-prone, so Rust provides string slices for this kind of problem.

A string slice is a reference to part of a string.

Adding & in front of the original string name indicates a reference to it, and adding [start_index..end_index] after it indicates a reference to part of that string. Note that the range inside [] is left-closed, right-open, so the end index is the next index after the end position of the slice. In plain terms: include the left, exclude the right.

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];
}
Enter fullscreen mode Exit fullscreen mode

In this example, the index range from 0 to 5 in s (including 0 but not including 5), namely "Hello", is assigned to the hello variable; the index range from 6 to 11 (including 6 but not including 11), namely "world", is assigned to the world variable.

As you can see from the diagram, the world variable does not exist independently of s, which allows the compiler to detect many potential problems during compilation.

Of course, there are also a few shorthand forms for indexing:

let hello = &s[0..5];
Enter fullscreen mode Exit fullscreen mode

This variable is sliced starting from index 0, and Rust allows this equivalent form:

let hello = &s[..5];
Enter fullscreen mode Exit fullscreen mode

let world = &s[6..11];
Enter fullscreen mode Exit fullscreen mode

This variable is sliced up to the last element of s, and Rust allows this equivalent form:

let world = &s[6..];
Enter fullscreen mode Exit fullscreen mode

If you want to slice the entire string, you can write:

let whole = &s[..];
Enter fullscreen mode Exit fullscreen mode

Notes

  • The range indices for string slices must fall on valid UTF-8 boundaries.
  • If you try to create a string slice from part of a multibyte character (it will be further discussed in the later chapter), the program will panic and exit.

Rewriting the Code

Now that we have learned slices, we can modify the code at the beginning of the article to optimize it further:

fn main() {
    let s = String::from("Hello world");
    let word = first_word(&s);
    println!("{}", word);
}
fn first_word(s:&String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        } 
    }
    &s[..]
}
Enter fullscreen mode Exit fullscreen mode
  • &str means a string slice.

If you add s.clear(); after the word = first_word(&s); line, Rust will detect the error and report it:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
Enter fullscreen mode Exit fullscreen mode

This is because a mutable reference s.clear() and an immutable reference &s appear in the same scope, violating the borrowing rules.
PS: s.clear() is equivalent to clear(&mut s)

4.5.3 String Literals Are Slices

String literals are stored directly in the binary program and are loaded into static memory when the program runs.

let s = "Hello, World!";
Enter fullscreen mode Exit fullscreen mode

The variable s has type &str, which is a slice pointing to a specific location in the binary program. &str is immutable, so string literals are immutable too.

4.5.4 Passing String Slices as Parameters

fn first_word(s:&String) -> &str {
Enter fullscreen mode Exit fullscreen mode

This is the line that declares the function in the optimized code we just wrote, and there is nothing wrong with this form itself. But experienced Rust developers use &str as the parameter type for s, because then the function can accept both String and &str arguments:

  • If the value you pass in is already a string slice, you can call it directly.
  • If the value is a String, you can pass an argument of type &String. When a function parameter needs &str and you pass &String, Rust will implicitly invoke Deref to convert &String into &str.

Using a string slice instead of a string reference as a function parameter makes the API more general without losing any functionality.

Based on this, we can further optimize the earlier code:

fn main() {
    let s = String::from("Hello world");
    let word = first_word(&s);
    println!("{}", word);
}
fn first_word(s:&str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[..i];
        } 
    }
    &s[..]
}
Enter fullscreen mode Exit fullscreen mode

This line:

let word = first_word(&s);
Enter fullscreen mode Exit fullscreen mode

can also be written as:

let word = first_word(&s[..]);
Enter fullscreen mode Exit fullscreen mode

For the former, Rust will implicitly invoke Deref and convert &String into &str; the latter manually converts it to &str.

4.5.5 Slices of Other Types

fn main() {  
    let number = [1, 2, 3, 4, 5];  
    let num = &number[1..3];  
    println!("{:?}", num);  
}
Enter fullscreen mode Exit fullscreen mode

Arrays can also use slices. The essence of the num slice is that it stores the pointer to the starting point of the slice in number (index 1 in this example) and the length information.

The output is:

[2, 3]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)