In Rust, a slice is a contiguous block of homogeneously typed data of varying length.
What does this mean?
[u8] is a slice. In memory, this is a block of u8s. The slice itself is the data. Many times though, people refer to &[u8] as a slice. A &[u8] is a reference to that block of data. That reference contains two things: a pointer to the data itself, and the length of the data. Since it contains two things, it is therefore called a fat pointer. A &u8 is also a reference (can also be thought of as a pointer in this case *), but we already know that whatever it points to will be a single u8. Therefore, it is a thin pointer since it only has one element.
You are guaranteed that all the data in a [u8] is of type u8.
Since your [u8] is just defined as a contiguous block of memory of type u8, there's no compile time definition as to how large it is. Hence, we need to store its length in a pointer to it. We also can't put it on the stack (This translates to: we can't have a local variable that is just a [u8] **).
Expanding:
- A
[T] is a slice of Ts. For any given T, as long as T is itself a sized type ***, we can imagine a type [T].
- A
str is a slice of a string. It is guaranteed to be valid UTF-8 text, and that's everything that separates it from a [u8]. Rust could have dumped the valid UTF-8 guarantee and just defined everything else in str as part of [u8].
Well, since you can't own a slice locally ****, you might be wondering how we create slices.
The answer is that we put the data in something with the size already known, and then borrow slices from that.
Take for example:
let my_array: [u32; 3] = [1, 2, 3];
We can slice my_array into a [u32] like so:
let my_slice: [u32] = my_array[..];
But since we can't own a local variable whose size isn't already known, we must put it under a reference:
let my_slice: &[u32] = &my_array[..];
The point of a slice, is that it's a very flexible (barring lifetimes) method of working with contiguous blocks of data, no matter where the data comes from. I could've just as easily made my_array a Vec<u8>, which is heap-allocated, and it would still have worked.
What is the difference between &String and &str?
&String is a reference to the whole string object. The string object in Rust is essentially a Vec<u8>. A Vec contains a pointer to the data it "contains", so your &String could be thought of as a &&str. And, that is why we could do either of the following:
let my_string: String = "Abc".to_string();
let my_str: &str = &my_string[..]; // As explained previously
// OR
let my_str: &str = &*my_string;
The explanation of this brings me to your last question:
What is the deref trait?
The Deref trait, is a trait which describes the dereference (*) operator. As you saw above, I was able to do *my_string. That's because String implements Deref, which allows you to dereference the String. Similarly, I can dereference a Vec<T> into a [T].
Note however, that the Deref trait is used in more places than just where * is used:
let my_string: String = "Abc".to_string();
let my_str: &str = &my_string;
If I try to assign a value of type &T into a place of type &U, then Rust will try to dereference my T, as many times as it takes to get a U, while still keeping at least one reference. Similarly, if I had a &&&&....&&&&T, and I tried to assign it to a &&&&....&&&&U, it would still work.
This is called deref coercion: automatically turning a &T into a &U, where some amount of *T would result in a U.
- *: Raw pointers
*const T and *mut T are the same size as references, but are treated as opaque by the compiler. The compiler doesn't make any guarantees about what is behind a raw pointer, or even that they're correctly aligned. Hence, they are unsafe to dereference. But since the Deref trait defines a deref method which is safe, dereferencing a raw pointer is special, and will not be done automatically either.
- **: This includes other dynamically sized types too, such as trait objects, and
extern types. This also includes structs which contain a dynamically sized type as their last member as well, although these are very difficult to correctly construct, but will become easier in the future with the CoerceUnsized trait. It is possible to invalidate all of this (Except for extern types) with the use of the unsized_locals nightly feature which allows some use of dynamically sized locals.
- ***: Sized types are all types whose size is known at compile time. You can identify them generically; given a type
T, T's size is known at compile time if T: Sized. If T: ?Sized, then its size may not be known at compile time (T: ?Sized is the most flexible requirement for callers since it accepts anything). Since a slice requires the data inside to be contiguous, and homogenous in size and type, dynamically sized types (Or !Sized) aren't possible to contain within a slice, or an array, or a Vec<T>, and maintain O(1) indexing. While Rust could probably write special code for indexing into a group of dynamically sized types, it currently doesn't.
- ****: You actually can own a slice, it just has to be under a pointer which owns it. This can be, for example, a
Box<[T]>, or a Rc<[T]>. These will deallocate the slice on their own (A Box when dropped, and a Rc when all strong and weak references of an Rc are dropped (The value's destructor is called when all strong references are dropped, but the memory isn't freed until all weak references are gone, too.)).