Skip to content

Slices

A slice is a pointer and a length.

A slice is a pointer and a length.

var items = [_]i32{ 10, 20, 30 };
const s: []i32 = items[0..];

The type []i32 means “slice of mutable i32 values.” The slice does not own the array. It only refers to part of it.

The length is stored in the slice.

const std = @import("std");

pub fn main() void {
    var items = [_]i32{ 10, 20, 30 };
    const s: []i32 = items[0..];

    std.debug.print("{d}\n", .{s.len});
}

This prints:

3

The elements are indexed in the usual way.

std.debug.print("{d}\n", .{s[0]});

A slice checks its bounds when safety checks are enabled. If the slice has length 3, valid indexes are 0, 1, and 2.

const x = s[3]; // out of bounds

This is an error in safe builds.

A slice can refer to the whole array.

const all = items[0..];

It can also refer to part of the array.

const middle = items[1..3];

The slice middle contains the elements at indexes 1 and 2. The end index is not included.

items[1..3] // items[1], items[2]

Changing a mutable slice changes the original array.

const std = @import("std");

pub fn main() void {
    var items = [_]i32{ 10, 20, 30 };
    const s = items[0..];

    s[1] = 99;

    std.debug.print("{d}\n", .{items[1]});
}

This prints:

99

There is still only one array. The slice is another view of its storage.

A slice may be constant.

const s: []const i32 = items[0..];

The type []const i32 means that the elements may be read through the slice, but not written through it.

const x = s[0]; // ok
s[0] = 1;       // error

Use []const T for function parameters when the function only reads the elements.

fn sum(items: []const i32) i32 {
    var total: i32 = 0;

    for (items) |item| {
        total += item;
    }

    return total;
}

Use []T when the function may change the elements.

fn zero(items: []i32) void {
    for (items) |*item| {
        item.* = 0;
    }
}

In this loop, |*item| captures a pointer to each element. That permits assignment to the element itself.

Without the *, the loop value is a copy.

for (items) |item| {
    // item is a value
}

With the *, it is a pointer to the element.

for (items) |*item| {
    item.* = 0;
}

Slices are the normal way to pass arrays to functions in Zig. They carry their own length, so the function does not need a separate length argument.

const std = @import("std");

fn printAll(items: []const i32) void {
    for (items) |item| {
        std.debug.print("{d}\n", .{item});
    }
}

pub fn main() void {
    const a = [_]i32{ 1, 2, 3 };
    printAll(a[0..]);
}

A string literal is a slice of constant bytes.

const name: []const u8 = "zig";

Its length is the number of bytes.

std.debug.print("{d}\n", .{name.len});

For "zig", the length is 3.

A slice may be empty.

const empty = items[0..0];

An empty slice has length zero. It still has a pointer value, but no element may be indexed.

empty.len // 0

Slices are small values. Passing a slice copies the pointer and the length. It does not copy the elements.

This is why slices are used so often: they are cheap to pass, explicit about length, and clear about mutability.

The common forms are:

[]T        // mutable slice
[]const T  // constant slice

Use a slice when a function needs many adjacent values and the number of values matters.

Exercise 5-13. Write sum for []const i32.

Exercise 5-14. Write fill that takes []u8 and a byte, and assigns that byte to every element.

Exercise 5-15. Write max that takes []const i32 and returns the largest value.

Exercise 5-16. Create an array of five integers, take a slice of the middle three, and modify them through the slice.