A slice is a view into a sequence of values.
It does not own the values. It points to values stored somewhere else.
const values = [_]i32{ 10, 20, 30, 40 };
const part = values[1..3];The slice part refers to this part of the array:
20, 30It does not copy those values. It only describes where they are and how many there are.
What a Slice Contains
A slice contains two pieces of information:
pointer to the first element
lengthSo this type:
[]const i32means:
a slice of constant i32 valuesThis type:
[]i32means:
a slice of mutable i32 valuesThe difference matters.
A mutable slice lets you change the values through the slice. A constant slice lets you read the values, but not change them.
Creating a Slice from an Array
Use range syntax:
const values = [_]i32{ 10, 20, 30, 40 };
const all = values[0..];
const first_two = values[0..2];
const middle = values[1..3];The ranges mean:
| Expression | Values |
|---|---|
values[0..] | 10, 20, 30, 40 |
values[0..2] | 10, 20 |
values[1..3] | 20, 30 |
The start index is included. The end index is excluded.
So:
values[1..3]means:
start at index 1
stop before index 3That gives indexes 1 and 2.
Slice Length
A slice has a .len field.
const values = [_]i32{ 10, 20, 30, 40 };
const part = values[1..3];
const n = part.len;Here, part.len is 2.
You can loop over a slice the same way you loop over an array:
const std = @import("std");
pub fn main() void {
const values = [_]i32{ 10, 20, 30, 40 };
const part = values[1..3];
for (part) |value| {
std.debug.print("{}\n", .{value});
}
}Output:
20
30Slices Do Not Own Memory
This is the most important rule.
A slice refers to memory owned by something else.
var values = [_]i32{ 10, 20, 30, 40 };
var part = values[1..3];
part[0] = 99;Now values contains:
10, 99, 30, 40The slice did not contain a separate copy. It pointed into the original array.
This is why slices are useful. You can pass part of an array to a function without copying it.
Passing Slices to Functions
A function that accepts a slice can work with many lengths.
fn sum(values: []const i32) i32 {
var total: i32 = 0;
for (values) |value| {
total += value;
}
return total;
}This function accepts any number of i32 values.
const a = [_]i32{ 1, 2, 3 };
const b = [_]i32{ 10, 20, 30, 40, 50 };
const x = sum(a[0..]);
const y = sum(b[1..4]);The first call passes all of a.
The second call passes:
20, 30, 40This is more flexible than a function that accepts a fixed array:
fn sumThree(values: [3]i32) i32 {
return values[0] + values[1] + values[2];
}sumThree accepts exactly 3 values. sum accepts any length.
Constant Slices
A constant slice prevents mutation through the slice.
fn printAll(values: []const i32) void {
for (values) |value| {
std.debug.print("{}\n", .{value});
}
}The function can read values, but it cannot do this:
values[0] = 99;Use []const T when a function only needs to read the data.
This is a good default for function parameters.
fn contains(values: []const i32, target: i32) bool {
for (values) |value| {
if (value == target) return true;
}
return false;
}The function does not need to modify the slice, so []const i32 is the right type.
Mutable Slices
Use []T when a function needs to modify values.
fn fill(values: []i32, replacement: i32) void {
for (values) |*value| {
value.* = replacement;
}
}Usage:
var values = [_]i32{ 1, 2, 3, 4 };
fill(values[0..], 0);Now the array contains:
0, 0, 0, 0The loop uses:
|*value|This captures each element by pointer, so the function can write to it.
Then:
value.* = replacement;writes into the element.
Slicing a Slice
You can create a smaller slice from an existing slice.
const values = [_]i32{ 10, 20, 30, 40, 50 };
const a = values[0..];
const b = a[1..4];
const c = b[1..];The values are:
| Name | Values |
|---|---|
a | 10, 20, 30, 40, 50 |
b | 20, 30, 40 |
c | 30, 40 |
All of these slices refer to the same original array.
No values are copied.
Bounds Checking
Zig checks slice indexes in safe build modes.
const values = [_]i32{ 10, 20, 30 };
const bad = values[1..5];This is invalid because index 5 is past the end of the array.
For a slice of length 3, valid indexes are:
0, 1, 2For slicing ranges, the end may equal the length.
const ok = values[1..3];This is valid because it stops before index 3.
But this is invalid:
const bad = values[1..4];Index 4 is beyond the end.
Empty Slices
A slice can be empty.
const values = [_]i32{ 10, 20, 30 };
const empty = values[1..1];The start and end are the same, so the slice has length zero.
empty.len == 0An empty slice is valid. It simply has no elements.
This is useful because many functions can handle empty input naturally.
fn sum(values: []const i32) i32 {
var total: i32 = 0;
for (values) |value| {
total += value;
}
return total;
}If values is empty, the loop runs zero times and the result is 0.
Slice Syntax Review
Here are the common forms:
| Syntax | Meaning |
|---|---|
array[0..] | Slice from index 0 to the end |
array[start..end] | Slice from start up to but not including end |
slice[start..end] | Smaller slice from an existing slice |
array[index] | One element, not a slice |
Do not confuse these:
values[1]This gives one value.
values[1..2]This gives a slice containing one value.
The types are different.
If values is an array of i32, then:
values[1]has type:
i32But:
values[1..2]has type:
[]const i32or:
[]i32depending on whether the original data is constant or mutable.
Slices and Strings
In Zig, strings are commonly represented as slices of bytes.
[]const u8means:
read-only sequence of bytesMany functions that accept text use []const u8.
Example:
const std = @import("std");
fn greet(name: []const u8) void {
std.debug.print("Hello, {s}!\n", .{name});
}
pub fn main() void {
greet("Zig");
}Output:
Hello, Zig!The formatting marker {s} prints a byte slice as a string.
A string literal can be passed where []const u8 is expected.
Slices and Lifetime
A slice must not outlive the memory it points to.
This is invalid in spirit:
fn bad() []const i32 {
const values = [_]i32{ 1, 2, 3 };
return values[0..];
}The array values lives inside the function. When the function returns, that local array is gone. Returning a slice to it would leave the caller with a pointer to invalid memory.
Zig tries to catch many lifetime mistakes, but you should learn the rule early:
A slice is only valid while the original storage is valid.
Good examples of valid storage:
global constant data
caller-owned arrays
heap allocations that are still alive
buffers that remain in scopeBad examples:
local arrays that disappear after return
temporary buffers that get freed
memory owned by an allocator after deallocationSlices and Allocation
A slice itself does not allocate memory.
This function does not allocate:
fn firstHalf(values: []const i32) []const i32 {
return values[0 .. values.len / 2];
}It returns a view into the original slice.
This is cheap. It only creates another pointer and length pair.
Allocation happens only when you explicitly ask an allocator for memory.
const buffer = try allocator.alloc(u8, 1024);That returns a slice of newly allocated memory:
[]u8The allocator owns the memory until you free it.
defer allocator.free(buffer);Now the slice buffer points to heap memory.
Common Mistake: Returning a Slice to Local Data
Do not do this:
fn makeName() []const u8 {
const name = [_]u8{ 'Z', 'i', 'g' };
return name[0..];
}The array disappears when the function returns.
Instead, use caller-provided memory, allocated memory, or constant data.
Constant data example:
fn name() []const u8 {
return "Zig";
}Caller-provided buffer example:
fn writeName(buffer: []u8) []u8 {
buffer[0] = 'Z';
buffer[1] = 'i';
buffer[2] = 'g';
return buffer[0..3];
}Common Mistake: Expecting a Copy
This code modifies the original array:
var values = [_]i32{ 1, 2, 3, 4 };
var part = values[1..3];
part[0] = 99;Afterward:
values = 1, 99, 3, 4A slice is a view, not a copy.
If you need a separate copy, allocate or create another array and copy the data.
Common Mistake: Using Mutable Slices When Read-Only Is Enough
This function should use []const i32:
fn printAll(values: []i32) void {
for (values) |value| {
std.debug.print("{}\n", .{value});
}
}It does not modify the slice, so write:
fn printAll(values: []const i32) void {
for (values) |value| {
std.debug.print("{}\n", .{value});
}
}This makes the function easier to call and safer to use.
A Complete Example
const std = @import("std");
fn sum(values: []const i32) i32 {
var total: i32 = 0;
for (values) |value| {
total += value;
}
return total;
}
fn fill(values: []i32, replacement: i32) void {
for (values) |*value| {
value.* = replacement;
}
}
pub fn main() void {
var numbers = [_]i32{ 10, 20, 30, 40, 50 };
const middle = numbers[1..4];
std.debug.print("middle sum = {}\n", .{sum(middle)});
fill(numbers[0..2], 0);
for (numbers) |value| {
std.debug.print("{} ", .{value});
}
std.debug.print("\n", .{});
}Output:
middle sum = 90
0 0 30 40 50The slice middle refers to 20, 30, 40.
The call:
fill(numbers[0..2], 0);modifies the first two elements of the original array.
Summary
A slice is a pointer plus a length.
It is written like this:
[]Tor:
[]const TUse []const T when you only need to read values. Use []T when you need to modify values.
A slice does not own memory. It points to memory owned by an array, an allocation, a string literal, or some other storage.
Slices are one of the most important types in Zig. They let functions work with data of many lengths without copying the data.