zig-arrays-vs-slices

Arrays and Slices in Zig: Key Differences and Use Cases

  • 4 min

An array in Zig is a fixed-size collection whose size is part of the type. If you come from JavaScript, Python, or C#, you probably think of an “Array” as a dynamic list, but in Zig we need to separate arrays and slices.

In Zig, it’s useful to separate two concepts:

  1. Array ([N]T): holds the data within the value itself and its size is known at compile time.
  2. Slice ([]T): is a view of data stored elsewhere; it carries a pointer and a length.

Understanding this difference helps avoid copies and express data ownership.

The Array ([N]T)

An array in Zig is a contiguous block of memory with a fixed size. The key point here is that the size is part of the type.

This means an array of 5 integers ([5]i32) is a different data type than an array of 4 integers ([4]i32). They are not compatible with each other.

// Declaring an array
const numbers = [5]i32{ 1, 2, 3, 4, 5 };

// Size inference with '_'
// The compiler counts the elements for us
const vowels = [_]u8{ 'a', 'e', 'i', 'o', 'u' };
Copied!

Array Characteristics

  • Contains its elements inline: the exact location depends on where and how the value is declared.
  • Has value semantics: when assigned to another variable, its elements are copied.
var a = [_]u8{ 1, 2, 3 };
var b = a; // FULL COPY!
b[0] = 9;

// 'a' is still {1, 2, 3}
// 'b' is {9, 2, 3}
Copied!

Copying a large array can be expensive. To share a view of its elements without copying the data, we use slices.

The Slice ([]T)

A slice does not store the elements by itself. It is a view that points to data stored elsewhere.

Internally, a Slice is composed of two things:

  1. A pointer to the first element.
  2. A len length that indicates how many elements the view contains.

We can think of the Array as the “physical terrain” and the Slice as the “camera” that focuses on a part of that terrain.

const real_array = [_]u8{ 10, 20, 30, 40, 50 };

// Create a slice that sees the entire array
const full_slice: []const u8 = &real_array;

// Create a slice that sees only from index 1 to 2 (exclusive of index 3)
const partial_view: []const u8 = real_array[1..3];
// partial_view points to { 20, 30 } and its len is 2
Copied!

Slicing Syntax [start..end]

The syntax start..end uses a half-open interval:

  • start is inclusive.
  • end is exclusive.
  • The starting index must be specified.
  • If you omit end, the slice extends to the end.
const everything = array[0..]; // Slice of the entire array
Copied!

Why Slices Are So Important

The interesting thing about Slices is that they allow us to write functions that are generic with respect to size.

If we write a function that accepts an array, we are tied to that size:

// Only accepts arrays of EXACTLY 5 integers
fn process(data: [5]i32) void { ... }
Copied!

But if we accept a Slice, the function accepts any amount of data:

// Accepts ANY amount of integers
fn process(data: []const i32) void {
    for (data) |elem| {
        // ...
    }
}

pub fn main() void {
    const small = [_]i32{1, 2};
    const large = [_]i32{1, 2, 3, 4, 5, 6};
    
    // Zig automatically converts the pointer to the array into a slice
    process(&small);
    process(&large);
}
Copied!

Practical idea: when a function needs to accept a variable number of contiguous elements, it’s usually best to receive a slice ([]T or []const T). This avoids tying it to a specific length.

Bounds Checking

Reading outside the bounds of an array or slice is illegal behavior. Zig incorporates bounds checking in safe modes like Debug and ReleaseSafe.

If the index is only known at runtime, these modes stop the program with a panic before accessing another memory area.

const numbers = [_]u8{ 1, 2, 3 };
const slice = numbers[0..];

// In Debug and ReleaseSafe, this causes a runtime panic.
const out_of_range_value = slice[5];
Copied!

If the index is known at compile time, Zig shows a compile error. In ReleaseFast and ReleaseSmall, several safety checks are disabled by default, so we should not rely on panic to validate external data.

Slices and Pointers

It is important to note the difference in type declarations:

  • *T: A pointer to a single element.
  • [*]T: A pointer to “many” elements (C-style, without known length, dangerous).
  • []T: A Slice (Pointer + Length, safe).

When the length is available, a slice is preferable to a pointer to many elements because it preserves that information and allows bounds checking.