Deref Coercion and Slices
Idiomatically, if we were going accept a Vec<i32> as an argument, we’d likely want to accept a slice instead. This is because many “container” types can easily be turned into a slice of their items. Accepting a slice as an argument places fewer restrictions on how the data passed into the function is stored.
Consider the following example, which has one key difference from the shared references example above: The argument to prints_numbers is a slice of i32.
fn main() {
let numbers = vec![10, 20, 30];
prints_numbers(&numbers);
prints_numbers(&numbers);
}
fn prints_numbers(vec_of_numbers: &[i32]) {
dbg!(vec_of_numbers);
}
A slice is a view into another container. In this case, we’re passing a slice of all of the Vec items into the prints_numbers function. This produces the same output we’ve been seeing, both calls to the prints_numbers function prints all of the Vec items.
❯ cargo run
[src/main.rs:8:5] vec_of_numbers = [
10,
20,
30,
]
[src/main.rs:8:5] vec_of_numbers = [
10,
20,
30,
]
This is possible because a Vec implements Deref for slice. We’ll talk more about traits like Deref later in more depth. The important information here is that there is an explicit opt-in to be able to deref a &Vec<i32> to a &[i32]. (read as: to deref a shared reference to a Vec of i32 into a slice).
As an ergonomic improvement, deref coercion enables Rust to use the Deref implementation that is defined to coerce a &Vec<i32> into a &[i32]. This happens in our example because the function that we wrote accepts a slice, and the argument we passed in was a shared reference to a Vec.
sub-slices
We could also use a sub-slice, which slices a sub-section of the items in Vec. Slices can be taken with range syntax, starting at index 0. In this case we use two ranges, one for each function call:
- an exclusive range,
0..1, which results in viewing a slice of one item, the item at index0, because the end (index1) is excluded - an inclusive range,
1..=2, which views the items at indices1and2
fn main() {
let numbers = vec![10, 20, 30];
prints_numbers(&numbers[0..1]);
prints_numbers(&numbers[1..=2]);
}
fn prints_numbers(vec_of_numbers: &[i32]) {
dbg!(vec_of_numbers);
}
This results in the slice containing the first item being printed out by the first function call, and the second function call prints out the second two items. These are completely separate slices that could include of the items.
❯ cargo run
[src/main.rs:8:5] vec_of_numbers = [
10,
]
[src/main.rs:8:5] vec_of_numbers = [
20,
30,
]
Owned and shared types
There are many types that implement Deref and thus have “owned” and shared variants that can take advantage of deref coercion.
A String owns its data, similar to a Vec. Thus similar to the way we can take a slice of the vec, we can take a string slice (&str).
fn main() {
let letters = String::from("abc");
prints_letters(&letters[0..1]);
prints_letters(&letters[1..=2]);
}
fn prints_letters(string_slice: &str) {
dbg!(string_slice);
}
Note however, that because Rust’s Strings are utf-8, they don’t implement indexing, only slice indexing. This means that taking slices of Strings likely isn’t what you want to do (unless you know you have ascii-only content), as you can end up in the middle of utf-8 code point boundaries. There are nice crates like unicode-segmentation which handle utf-8 appropriately.
Another type that has a similar configuration is PathBuf and Path, where PathBuf is the owned data type and Path is like the slices.
use std::{
path::{Path, PathBuf},
str::FromStr,
};
fn main() {
let path = PathBuf::from_str(
"/Users/rust-adventure/workshops",
)
.unwrap();
prints_path(&path);
prints_path(&path);
}
fn prints_path(path: &Path) {
dbg!(path);
}
Here we create an owned PathBuf from a string literal, which could fail, so we unwrap. We’ll talk more about error handling later in a dedicated section. This PathBuf implements Deref for Path, so we can again take advantage of deref coercion.
However, this time, PathBuf doesn’t implement Index or SliceIndex so we can’t take sub-slices.
Copy Semantics
So far we’ve talked about Rust’s default behavior: move semantics. This is what happens when we move our Vec through the prints_numbers function from earlier. Ownership passes from the initial variable into the function, and so on.
Sometimes this is not what you want. For example, integers are often used only for the value of the integer. Here’s an example, where the assignment implicitly copies the number, creating a second copy. This behavior has to be opted into by implementing the Copy trait, which can only be implemented for types that are cheap to copy.
fn main() {
let mut number = 5;
let second = number;
number += 5;
dbg!(number, second);
}