Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

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()
}
  • 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];
}

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. String s and slice world pointing into the same hello world heap buffer 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:

#![allow(unused)]
fn main() {
let hello = &s[0..5];
}

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

#![allow(unused)]
fn main() {
let hello = &s[..5];
}
#![allow(unused)]
fn main() {
let world = &s[6..11];
}

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

#![allow(unused)]
fn main() {
let world = &s[6..];
}

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

#![allow(unused)]
fn main() {
let whole = &s[..];
}

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, 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 mut 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[..]
}
  • &str means a string slice.

If you insert s.clear(); between the let word = first_word(&s); line and the println! that uses word, Rust will detect the error and report it:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:4:2
  |
3 |     let word = first_word(&s);
  |                           -- immutable borrow occurs here
4 |     s.clear();
  |     ^^^^^^^^^ mutable borrow occurs here
5 |     println!("{}", word);
  |                    ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `slice-clear` (bin "slice-clear") due to 1 previous error

This is because a mutable borrow from s.clear() overlaps with the immutable borrow held by word, 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.

#![allow(unused)]
fn main() {
let s = "Hello, World!";
}

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

#![allow(unused)]
fn main() {
fn first_word(s:&String) -> &str {
}

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[..]
}

This line:

#![allow(unused)]
fn main() {
let word = first_word(&s);
}

can also be written as:

#![allow(unused)]
fn main() {
let word = first_word(&s[..]);
}

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);
}

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]