Arrays and Slices

Learn about the most common data types and accessing patterns in Rust.

Let’s quickly review what arrays and slices are.

Arrays

An array is a collection of a fixed number of elements of any single type T. The array type is [T, n], where T is any type, and n is the fixed number of elements. That is, the array’s size, which is known at compile time.

There are two ways to create arrays:

  • By listing each element (such as [A, B, C], where A, B, and C are elements of the same type).

  • By copy-repetitions, which is declaring an element and the number of copies to do of that element (such as [A; 3]). The element type must implement the Copy trait.

Here are some examples of array instantiation:

let array [f32; 3]= [1.1, 1.2, 1.3];
let array = [0i8; 3];

Arrays are 0-indexed. The first element is at index 0 and the elements are retrieved with a usize indexing notation. We can also assign values to elements of an array using the same notation, provided the array is mut.

let array = ["first", "second", "third"];
println!("Element 0 is: {}", array[0]);
let mut new_array: [i32; 4] = [0; 4];
new_array[1] = 1;
println!("Element 1 is: {}", new_array[1]);

We can print arrays with Debug.

In fact, the compiler assigns a Debug and a Default notation for arrays with up to 32 elements.

The following code works like we expect it to:

let array = [0i8; 32];
println!("{:?}", array);

However, the following code will not compile:

// Careful: this will not compile!
let array = [0i8; 33];
println!("{:?}", array);

If we run the code above, it will show the following compiler error:

error[E0277]: arrays only have std trait implementations for lengths 0..=32

The Default trait is also automatically implemented for arrays with up to 32 elements.

Slices

A slice is a type which lacks ownership. Slices are used to reference a contiguous sequence of elements in a collection.

Arrays naturally coalesce into a slice.

The canonical form for a slice is &array[slice], which will get a slice out of a whole collection.

For example:

let array = [1, 2, 3, 4, 5];
let slice = &array[1..3];
println!("Slice: {:?}", slice);

If we run the code above, we get:

Slice: [2, 3]

This means the slice indexes with the first element included and the second excluded.

Let’s check the behavior of the code below:

let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let slice = &array[1..2];
println!("Slice: {:?}", slice);
let slice = &array[..3];
println!("Slice: {:?}", slice);
let slice = &array[5..];
println!("Slice: {:?}", slice);
let slice = &array[..0];
println!("Slice: {:?}", slice);

Contrary to what we expected to happen, &array[..3]; prints:

Slice: [1, 2, 3]

On the other hand, &array[5..]; prints:

Slice: [6, 7, 8, 9, 10]

This seems counterintuitive until we remember that indexing is 0 based. In fact, the last code example produces an empty array because we have asked for a slice containing all previous elements of element 0.

We need to remember that slices lack ownership, which is why we can keep creating overlapping slices within the same array.

If slices had ownership, they could not be used as easily to access arrays.

Common array errors

People who are fluent in other programming languages (especially C/C++ or Python) may have some trouble getting used to looping in Rust. Let’s consider the following code:

// careful: it will not compile!
let magic_choice = [0, 1, 2];
let magic_answer = ["first", "second", "third"];
for i in magic_choice {
println!("{}", magic_answer[i]);
}

Conceptually, this process is not that complicated. We use an array of magic_choice (or indexes) to get the elements of magic_answer (our target array).

We can find this same construct in many different scenarios, such as when we leave the choice of the array up to the user.

Here, however, we get an error:

error[E0277]: `[{integer}; 3]` is not an iterator

What went wrong?

As it often does, the compiler comes to our rescue with a suggestion:

borrow the array with `&` or call `.iter()` on it to iterate over it

Nice! Let’s try to iterate over the array:

// careful: it will not compile!
let magic_choice = [0, 1, 2];
let magic_answer = ["first", "second", "third"];
for i in magic_choice.iter() {
println!("{}", magic_answer[i]);
}

The code above will not compile and will return the following error:

error[E0277]: the type `[&str]` cannot be indexed by `&{integer}`

The compiler inferred the integer type for the magic_choice array. But arrays are indexed by usize.

We could indeed cast the i element to a usize:

// careful: it will not compile!
let magic_choice = [0, 1, 2];
let magic_answer = ["first", "second", "third"];
for i in magic_choice.iter() {
println!("{}", magic_answer[i as usize]);
}

However, we still have a faulty code. The code above returns this error instead of compiling:

error[E0606]: casting `&i32` as `usize` is invalid

Once again, the compiler provides a helpful suggestion:

cannot cast `&i32` as `usize`
help: dereference the expression: `*i`

With this information, we can correct the code to the following, which will finally compile:

// This will compile!
let magic_choice = [0, 1, 2];
let magic_answer = ["first", "second", "third"];
for i in magic_choice.iter() {
println!("{}", magic_answer[*i as usize]);
}

Many new rustaceans (Rust programmers) find themselves wrestling with the compiler. There’s nothing wrong with this process, as long as we try to understand the errors we are making at each step.

Now that our code works correctly, let’s look back at the errors we made:

  • We looped the wrong way (without .iter())
  • We didn’t index with usize
  • We cast the wrong way
  • We didn’t dereference properly

These problems started because the compiler inferred that magic_choice was an integer array.

The example above is useful for two reasons:

  1. It shows a way to correct the code as we go, with the help of the compiler.
  2. It demonstrates how setting everything to the wrong kind of integer can cause an error. People who are already comfortable with other programming languages often make this mistake.

Another common error that programmers make is trying to impose the type of the data in Rust. That happens because if we are used to setting indexes with integers in C or other similar languages, we usually loop with int.

Funnily enough, we could have made the compiler infer the right type from the beginning, like this:

// This will compile!
let magic_choice = [0, 1, 2];
let magic_answer = ["first", "second", "third"];
for i in magic_choice.iter() {
println!("{}", magic_answer[*i]);
}

If we had just dereferenced without casting, the compiler would have understood that we meant to have an array of usize.

Casting everything is common in C/C++. People with a strong background in C often try to cast everything, hoping to make the compiler understand what they are trying to accomplish.

However, these extra steps are completely unnecessary if we explain what the initial data is to the compiler before using it, rather than casting after the compiler has inferred the wrong type.

In fact, we could have been explicit right from the beginning!

Check out the following code:

// This will compile!
let magic_choice: [usize; 3] = [0, 1, 2];
let magic_answer = ["first", "second", "third"];
for i in magic_choice.iter() {
println!("{}", magic_answer[*i]);
}

In this case, we declared the right type at the beginning, and everything went smoothly afterward.

The slice pattern

One of the most useful constructs of Rust is its ability to match patterns.

However, we can also match using a slice pattern!

The slice pattern extracts elements out of an array:

let point = [1.22, 3.3];
let [x, y] = point;
println!("X: {}", x);
println!("Y: {}", y);

But how can we apply this process to slice pattern matching?

Let’s consider the following code:

fn print_elements(elements: Vec<&str>) {
match elements.as_slice(){
[] => println!("No elements"),
[element] => println!("One element only: {}", element),
_ => println!("More than one element present"),
}
}
print_elements(vec![]);
print_elements(vec!["one"]);
print_elements(vec!["one", "two"]);

In the code above, we match against a vector of &str as if it was a slice (as_slice()).

Note: The following lesson assumes that you are already familiar with vectors.

Let’s break down how our example code works:

  • The first arm of the match is an empty slice, so it matches against empty vectors.
  • The second arm is a slice pattern with one element. It extracts and prints other such elements.
  • The third arm matches against any vector with more than one element.

Consider this modification of the code:

fn print_elements(elements: Vec<&str>) {
match elements.as_slice(){
[] => println!("No elements"),
[element] => println!("One element only: {}", element),
[element1, element2] => println!("Two elements\n1: {}\n2: {}", element1, element2),
_ => println!("More than one element present"),
}
}
let empty = vec![];
let one = vec!["one"];
let two = vec!["one", "two"];
let three = vec!["one", "two", "three"];
print_elements(empty);
print_elements(one);
print_elements(three);
print_elements(two);

As you can see, we can make a lot of progress just by simplifying complex matches with the right slice pattern.