Slices
A slice is a pointer plus a length. It is the type you should reach for by default when passing sequences around.
const std = @import("std");
const expect = std.testing.expect;
fn total(values: []const u8) u32 {
var sum: u32 = 0;
for (values) |v| sum += v;
return sum;
}
test "slicing an array" {
var array = [_]u8{ 1, 2, 3, 4, 5 };
const slice = array[1..4]; // end is exclusive
try expect(slice.len == 3);
try expect(total(slice) == 9);
}
test "slices share memory" {
var array = [_]u8{ 1, 2, 3 };
const slice = array[0..2];
slice[0] = 100;
try expect(array[0] == 100); // no copy was made
}
test "strings are slices" {
// A string literal is a `*const [N:0]u8` (a pointer to a
// null-terminated array), which coerces to `[]const u8`.
const message: []const u8 = "hello";
try expect(message.len == 5);
try expect(std.mem.eql(u8, message[0..2], "he"));
}
test "slicing to the end" {
const array = [_]u8{ 1, 2, 3, 4 };
const rest = array[2..];
try expect(rest.len == 2);
}Slicing does not copy
array[1..4] produces a view into the same memory: writing through the slice
writes through to the array. The upper bound is exclusive, matching for
ranges. array[2..] slices to the end.
Because a slice borrows, its lifetime is tied to whatever it points at. A slice of a local array must not outlive that array; Zig will not catch this for you.
[]const T vs []T
Take []const T in function parameters unless you intend to mutate. It
documents intent and accepts more callers, since []T coerces to []const T
but not the reverse.
Strings are slices
Zig has no dedicated string type. A literal like "hello" is a
*const [5:0]u8 (a pointer to a null-terminated array), which coerces to
[]const u8. Consequences worth internalising:
.lenis the byte count, not a character count. UTF-8 is bytes here.- Compare with
std.mem.eql(u8, a, b), never==, which would compare pointers. - The null terminator is present for C interop but is not included in
.len.