What is the difference between a slice and an array?

Solution 1:

[T; n] is an array of length n, represented as n adjacent T instances.

&[T; n] is purely a reference to that array, represented as a thin pointer to the data.

[T] is a slice, an unsized type; it can only be used through some form of indirection.

&[T], called a slice, is a sized type. It's a fat pointer, represented as a pointer to the first item and the length of the slice.

Arrays thus have their length known at compile time while slice lengths are a runtime matter. Arrays are second class citizens at present in Rust, as it is not possible to form array generics. There are manual implementations of the various traits for [T; 0], [T; 1], &c., typically up to 32; because of this limitation, slices are much more generally useful. The fact that &[T; n] can coerce to &[T] is the aspect that makes them tolerable.

There is an implementation of fmt::Debug for [T; 3] where T implements Debug, and another for &T where T implements fmt::Debug, and so as u8 implements Debug, &[u8; 3] also does.

Why can &[T; n] coerce to &[T]? In Rust, when does coercion happen?

It will coerce when it needs to and at no other times. I can think of two cases:

  1. where something expects a &[T] and you give it a &[T; n] it will coerce silently;
  2. when you call x.starts_with(…) on a [T; n] it will observe that there is no such method on [T; n], and so autoref comes into play and it tries &[T; n], which doesn’t help, and then coercion come into play and it tries &[T], which has a method called starts_with.

The snippet [1, 2, 3].starts_with(&[1, 2]) demonstrates both.

Solution 2:

Why can &[T; n] coerce to &[T]?

The other answer explains why &[T; n] should coerce to &[T], here I'll explain how the compiler works out that &[T; n] can coerce to &[T].

There are four possible coercions in Rust:

  1. Transitivity.

    • If T coerces to U and U coerces to V, then T coerces to V.
  2. Pointer weakening:

    • removing mutability: &mut T&T and *mut T*const T
    • converting to raw pointer: &mut T*mut T and &T*const T
  3. Deref trait:

    • If T: Deref<Target = U>, then &T coerces to &U via the deref() method
    • (Similarly, if T: DerefMut, then &mut T coerces to &mut U via deref_mut())
  4. Unsize trait:

    • If Ptr is a "pointer type" (e.g. &T, *mut T, Box, Rc etc), and T: Unsize<U>, then Ptr<T> coerces to Ptr<U>.

    • The Unsize trait is automatically implemented for:

      • [T; n]: Unsize<[T]>
      • T: Unsize<Trait> where T: Trait
      • struct Foo<…> { …, field: T }: Unsize< struct Foo<…> { …, field: U }>, provided that T: Unsize<U> (and some more conditions to make the job easier for the compiler)
    • (Rust recognizes Ptr<X> as a "pointer type" if it implements CoerceUnsized. The actual rule is stated as, “if T: CoerceUnsized<U> then T coerces to U”.)

The reason &[T; n] coerces to &[T] is rule 4: (a) the compiler generates the implementation impl Unsize<[T]> for [T; n] for every [T; n], and (b) the reference &X is a pointer type. Using these, &[T; n] can coerce to &[T].

Solution 3:

I created this picture according to the answers of kennytm and Chris Morgan. It describes the various concepts:

enter image description here