References, arrays and the like
References
Rust has two types of references: "Normal" references that are managed by the compiler, and pointers that need to be managed by the programmer. Typically, you'll only see pointers around unsafe
code, as that is needed to work with them. In normal application development, you should rarely see unsafe code, if ever. However, it might make things easier (or possible at all) when going really low-level, like implementing fast, basic containers or doing embedded computing. Regular references are a major component of Rust's ownership system, but more on that in its own chapter.
Arrays
Like in other system programming languages, arrays in Rust have a static size. This greatly limits their use cases, but comes with its own advantages. Firstly, it allows for much better optimization, secondly, it can also convey intent when reading others' code. As you might have guessed, arrays with dynamic size are handled by the standard library. However, this is not the end of the story: Enter slices.
Slices
Technically, slices are pointers into arrays that also define the length. Conceptually, slices are views into an array (or a part of it), with no regard for it being statically or dynamically sized. They look like an array without a length parameter.
This brings about some guarantees: A slice points to one single block of continuous memory. As such, nothing can exist between elements and performance is very high when working with them.
As for creating a slice, you basically index an array with a range. A range can be open (grabbing the whole array), half-open or closed. A range is also including its lower index, but excluding its upper one, unless you add a =
after the range operator, yielding ..=
.
With these three elements, it's time for some more example code.
#![allow(unused)] fn main() { let answer: usize = 42; // Reference to the variable let my_reference: &usize = &answer; println!("{}", answer); println!("{}", *my_reference); println!(); let my_array: [usize; 8] = [42; 8]; // Copying initializer let other_array: [usize; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; for number in my_array { print!("{} ", number); } println!(); println!(); for number in other_array { print!("{} ", number); } println!(); println!(); // Extra prints are hidden to make the code clearer // Can you guess all outputs? let element: usize = my_array[3]; println!("{}", element); // 42 let element_reference: &usize = &my_array[3]; println!("{}", element_reference); // 42 let array_reference: &[usize; 8] = &my_array; // 42 42 42 42 42 42 42 42 for number in array_reference { print!("{} ", number); } println!(); let whole_array_slice: &[usize] = &other_array[..]; // 0 1 2 3 4 5 6 7 for number in whole_array_slice { print!("{} ", number); } println!(); let element_from_slice: usize = whole_array_slice[3]; println!("{}", element_from_slice); // 3 let another_whole_array: &[usize] = &other_array[0..8]; // 0 1 2 3 4 5 6 7 for number in another_whole_array { print!("{} ", number); } println!(); let array_front: &[usize] = &other_array[..=4]; // 0 1 2 3 4 for number in array_front { print!("{} ", number); } println!(); let array_back: &[usize] = &other_array[4..]; // 4 5 6 7 for number in array_back { print!("{} ", number); } println!(); // Attention, it's relative to the slice, not the array! let element_from_back: usize = array_back[1]; println!("{}", element_from_back); // 5 }
One thing to note here: Rust checks indices for being out of bounds, so instead or accessing invalid memory, the program will panic. For arrays and constant indices, Rust will even check the bounds at compile time. The amount of errors this prevents cannot be overstated.
You can skip these checks in highly optimized code if you really want to, but then you are responsible for it never accessing invalid memory, so of course that's unsafe
. For regular code, correctness far outweighs the potential performance gain of not checking bounds.
Coming back to chars, I promised to explain how strings are handled efficiently, even though a single char
is 32 bits already. There's a special slice called str
, using the UTF-8 codec to only save as many bytes as needed.
Naively indexing into it could lead to errors very fast since you can hit a byte in the middle of a symbol, so there are safe methods providing access to single chars, as well as converting the slice into a u8
slice for more general data processing.
Using str
slices is fine for constant strings, much like const char *
in C++, but as slices only provide a view into data and don't own it, you will work just as much with the owning String
struct from the standard library in most cases.
An upcoming chapter will deal with strings in even more detail.