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], whereTis the type of the elements in the slice. -
Immutable slices:
&[T], which only allow read operations. -
Mutable slices:
&mut [T], which allow modification.
- Slice types are represented as
-
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
Stringelement by element and check whether each value is a space, you use theas_bytesmethod to convertStringinto a byte array. - We will talk about iterators later. For now, all you need to know is that
iteris a method used to retrieve each element in a collection one by one.enumerateis a tool that adds an index to each element on top ofiterand 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.
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];
This variable is sliced starting from index 0, and Rust allows this equivalent form:
let hello = &s[..5];
let world = &s[6..11];
This variable is sliced up to the last element of s, and Rust allows this equivalent form:
let world = &s[6..];
If you want to slice the entire string, you can write:
let whole = &s[..];
Notes
- The range indices for string slices must fall on valid
UTF-8boundaries. - 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[..]
}
-
&strmeans 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
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!";
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 {
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&strand you pass&String, Rust will implicitly invokeDerefto convert&Stringinto&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:
let word = first_word(&s);
can also be written as:
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]

Top comments (0)