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:
- where something expects a
&[T]
and you give it a&[T; n]
it will coerce silently; - 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 calledstarts_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:
-
Transitivity.
- If
T
coerces toU
andU
coerces toV
, thenT
coerces toV
.
- If
-
Pointer weakening:
- removing mutability:
&mut T
→&T
and*mut T
→*const T
- converting to raw pointer:
&mut T
→*mut T
and&T
→*const T
- removing mutability:
-
Deref
trait:- If
T: Deref<Target = U>
, then&T
coerces to&U
via thederef()
method - (Similarly, if
T: DerefMut
, then&mut T
coerces to&mut U
viaderef_mut()
)
- If
-
Unsize
trait:If
Ptr
is a "pointer type" (e.g.&T
,*mut T
,Box
,Rc
etc), andT: Unsize<U>
, thenPtr<T>
coerces toPtr<U>
.-
The
Unsize
trait is automatically implemented for:[T; n]: Unsize<[T]>
-
T: Unsize<Trait>
whereT: Trait
-
struct Foo<…> { …, field: T }: Unsize< struct Foo<…> { …, field: U }>
, provided thatT: Unsize<U>
(and some more conditions to make the job easier for the compiler)
(Rust recognizes
Ptr<X>
as a "pointer type" if it implementsCoerceUnsized
. The actual rule is stated as, “ifT: CoerceUnsized<U>
thenT
coerces toU
”.)
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: