Rust slices and indexing
Categories: Study Notes
(Eng) Rust slices and indexing
Three questions about the same underlying rule — sized vs unsized types.
How to set an array of zeros
let zeros: [i32; 5] = [0; 5];
The [val; N] repeat expression evaluates val once and copies it N times. Works for any Copy type.
let zeros: Vec<i32> = vec![0; 5]; // heap version
let zeros: [f64; 10] = [0.0; 10]; // or float
Embedded use case. Initialise a register buffer in one line, no memset:
let adc_buffer: [u16; 256] = [0; 256];
The gotcha. The length is part of the type — it must be a compile-time constant.
let n = 5;
// let bad: [i32; n] = [0; n]; // ❌ n is not a const
let good: Vec<i32> = vec![0; n]; // ✅ Vec is runtime-sized
Why &a[1..4] instead of a[1..4]
You cannot bind a range index by value:
let a = [10, 20, 30, 40, 50];
let slice = a[1..4]; // ❌ expected &[i32], found [i32]
The [] operator desugars into a method call:
// a[1..4] means *a.index(1..4)
// Index::index(&self, idx) -> &Self::Output
// So * dereferences &[i32] back to [i32]
// And [i32] is unsized — the compiler does not know its length at compile time
Unsized types cannot live on the stack. The fix is to borrow the result back into a fat pointer:
let slice = &a[1..4]; // ✅ &[i32] — two words: data pointer + length
&[i32] has a known size (16 bytes on 64-bit), so the stack can hold it.
C analogy. You don't copy array slices by value in C either:
int arr[5] = {10, 20, 30, 40, 50};
int *slice = &arr[1]; // pointer into the original
Rust's &[i32] is that same pointer, but with the length carried alongside — bounds-checked and no off-by-ones.
Panic-safe alternative.
let slice = a.get(1..4); // Option<&[i32]>
Returns None instead of panicking when the range is out of bounds.
Why size_of_val can trick you
let a: [u8; 4] = [10, 20, 30, 40];
size_of_val(&a) // 4 — size of the [u8; 4] value
size_of_val(&a[1..3]) // 2 — size of the [u8] slice (2 elements)
size_of_val looks through the reference and measures the pointee, not the pointer.
size_of::<&[u8; 4]>() // 8 — thin pointer (one word)
size_of::<&[u8]>() // 16 — fat pointer (data ptr + length)
size_of on the reference type measures the pointer itself.
| Expression | Size | What it measures |
|---|---|---|
size_of_val(&a) | 4 | the [u8; 4] value itself |
size_of_val(&a[1..3]) | 2 | the [u8] slice (2 elements) |
size_of::<&[u8; 4]>() | 8 | the thin pointer (1 word) |
size_of::<&[u8]>() | 16 | the fat pointer (data ptr + length) |
This is the same reason &a[1..4] is required — [u8] is unsized, but &[u8] is a known-size fat pointer the compiler can place on the stack.
The big picture
| Problem | C habit | Rust way |
|---|---|---|
| Zero-init array | int buf[256] = {0} | let buf = [0u16; 256] — repeat expr, no memset |
| Slice of existing array | int *p = &arr[1] | let s = &a[1..4] — [T] unsized, & makes &[T] |
| Check slice size | sizeof on array | size_of_val(&s) — measures pointee, not pointer |
The core lesson. Rust gives you the same low-level control as C — zero-cost slices, no hidden copies — but forces you to be explicit about sized vs unsized types. When the compiler says [i32] is unsized, it is telling you "you need a reference, not a bare slice value." Once that clicks, &a[1..4] becomes instinct.