Slices
Slices are like a view over an array. To declare a slice, we first need to have an existing array:
const name = a_existing[start_index .. end_index];
We can also omit the end index - in that case, the slice will go from the starting position to the end of the array. Let’s see how to define a slice in the following example:
slice.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const a_nums = [_]u8{ 11, 21, 3, 14, 35, 8 }; const z_range = a_nums[2..5]; print("Slice z_range: \n\t {any}\n", .{z_range}); print("\tFirst element of the slice: {}\n", .{z_range[0]}); print("\tSecond element of the slice: {}\n", .{z_range[1]}); print("\tThird element of the slice: {}\n", .{z_range[2]}); } |
|
$ zig run slice.zig |
|
Slice z_range: { 3, 14, 35 } First element of the slice: 3 Second element of the slice: 14 Third element of the slice: 35 |
Slices in memory
A slice can point to the data of an array, but that data doesn’t belong to that slice. An array always has a fixed length, defined at compile time when we write and build the Zig code. A slice, on the other hand, can have a different length depending on how the program runs.
We can represent the array a_nums with the following diagram:
fig. array a_nums |
The slice z_range was defined over the array, starting at index 2 and ending at 5:
fig. slice z_range |
||||||||||||||||||||||||||||||
As you can see from the program's output:
- The start index is included.
- The end index is not included (index 5 is not part of the slice).
- So, the slice actually contains elements from index 2 to 4 (5 -1).
The values at positions 2, 3, and 4 in the a_nums array now appear in the z_range slice at positions 0, 1, and 2. We now have two constants pointing to the same values in memory.
This means that if a_nums is a variable and we change any of its values, the slice pointing to that part of the array will reflect those changes as well.
slice_array_mutable.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { var a_nums = [_]u8{ 11, 21, 3, 14, 35, 8 }; // create a slice pointing to indexes: 2,3,4 const z_range = a_nums[2..5]; print("Slice z_range: \n\t {any}\n", .{z_range}); print("\tElement 1 of the slice: {}\n", .{z_range[0]}); // change the first element of the slice z_range[0] = 55; print("\tElement 1 of the slice after change: {}\n", .{z_range[0]}); // change element at index 3 of the array (second in the slice) a_nums[3] = 13; print("\tElement 2 of the slice: {}\n", .{z_range[1]}); // change element at index 4 of the array (third in the slice) a_nums[4] = 42; print("\tElement 3 of the slice: {}\n", .{z_range[2]}); print("\nSlice z_range after the changes: \n\t {any}\n", .{z_range}); print("\nArray a_nums after the changes: \n\t {any}\n", .{a_nums}); } |
|
$ zig run slice_array_mutable.zig |
|
Slice z_range: { 3, 14, 35 } Slice element 1: 3 Slice element 1 after change: 55 Slice element 2: 13 Slice element 3: 42 Slice z_range, after the changes: { 55, 13, 42 } Array a_nums, after the changes: { 11, 21, 55, 13, 42, 8 } |
In this case, the array a_nums, being a variable, allows us to change its values.
Even though z_range was declared as a constant, it points to the same memory area: changes made to the original array elements are automatically reflected in the slice. We can also modify the values of those elements directly through the slice itself, as happens with:
|
// we change the first element of the slice z_range[0] = 55; |
It's essential to understand that slices are not copies of the array but active views over it. This will be useful when creating functions that accept segments of an array without copying anything unnecessary.
Now you’ll say, “But wait a second, if the slice is constant, how come we can change the values through the slice?”
See: a constant slice doesn't mean the values it points to can't be changed - that depends on the underlying array. A constant slice means that once it's declared, its range is fixed: its beginning and end can't be moved anymore.
A variable slice can change its start and end, but it must respect the original length of the slice: you can't change its size.
slice_mutable.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { var a_nums = [_]u8{ 11, 21, 3, 14, 35, 8 }; // we create a slice pointing to indices: 2,3,4 var z_range = a_nums[2..5]; print("Slice z_range: \n\t {any}\n", .{z_range}); print("\tElement 1 of the slice: {}\n", .{z_range[0]}); // now the slice points to indices: 0,1,2 z_range = a_nums[0..3]; print("Slice z_range: \n\t {any}\n", .{z_range}); print("\tElement 1 of the slice: {}\n", .{z_range[0]}); } |
|
$ zig run slice_mutable.zig |
|
Slice z_range: { 3, 14, 35 } Element 1 of the slice: 3 Slice z_range: { 11, 21, 3 } Element 1 of the slice: 11 |
Here, the slice z_range is mutable, so we can make it point to a different range of the original array. But in this case, we haven't modified the original array or created a new slice at any point.
Length of slices
In the following example, we imagine that in a game, there are 8 possible upgrades for a weapon, a shield, or a ship.
We define those upgrades with the array a_powerups. Depending on the experience points accumulated by the player, different upgrades can be selected using a slice:
slice_length.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const a_powerups = [_]u8{ 1, 2, 3, 4, 5, 7, 8 }; // the player has these experience points const n_exp = 3000; // depending on the experience points // the available powerups are different const z_available_powerups = if (n_exp > 2000) a_powerups[0..] else a_powerups[0..3]; print("With your experience points: {}\nYou can choose these powerups: \n\t {any}\n", .{ n_exp, z_available_powerups }); } |
|
$ zig run slice_length.zig |
|
With your experience points: 3000 You can choose these powerups: { 1, 2, 3, 4, 5, 7 } |
If we change the value of n_exp to 1000, for example:
|
const n_exp = 1000; |
|
$ zig run slice_length.zig |
|
With your experience points: 1000 You can choose these powerups: { 1, 2, 3 } |
In this case, the slice z_available_powerups has different content and length depending on the input data, so Zig can't know that length at compile time.
Repeating and concatenating arrays (or slices)
We can repeat an array or a slice using the ** operator followed by the number of times we want to repeat it:
|
const a_x = [_]u8{48}; // array with a single element (char '0') const a_pad = a_x ** 6; // we repeat the array 6 times
// a_pad = .{‘0’,’0’,’0’,’0’,’0’,’0’ } |
To concatenate arrays or slices, we use the ++ operator:
|
const s_x = "Hello" ++ " " ++ "programmer" // “Hello programmer” |
The data we repeat or concatenate must be known at compile time.
Imagine we need to display a score on an arcade machine that always shows the result with 8 digits. We'll need to pad the remaining digits on the left with 0s.
padded_score.zig |
|
const std = @import("std"); const print = std.debug.print; pub fn main() void { const n_score = 10007; const N_WIDTH = 8; // get how many times 10 fits into the score const n_has_zeros = std.math.log10(n_score); // calculate how many 0s to pad on the left const n_pad_with = N_WIDTH - n_has_zeros - 1; const z_zero = "0"; // string (slice in Zig) with a single '0' // et the left-padding zeros as a string // repeating the "0" slice to fill the empty spaces const s_score_pad = z_zero ** n_pad_with; const s_resp: []const u8 = std.fmt.comptimePrint("{s}{}", .{ s_score_pad, n_score }); // finally we concatenate two slices (strings) using ++ print("{s}: \n", .{"SCORE: " ++ s_resp}); } |
|
$ zig run padded_score.zig |
|
SCORE: 00010007 |
In the code, you'll notice we use the function std.fmt.comptimePrint.
|
const s_resp = std.fmt.comptimePrint("{s}{}", .{ s_score_pad, n_score }) |
It's a function from Zig's standard library, similar to std.debug.print. But instead of printing the interpolated string, it just returns it as a result.
Passing slices to functions
Slices are very useful for passing data to functions:
- You avoid copying information unnecessarily,
- You can work with parts of arrays without hassle,
- If the slice is const, it will be read-only; if it's mutable, you can modify the data (though that can also cause a lot of headaches in large programs)
In this example, a player reaches a certain skill level with weapons. The game tells them which weapons they can use:
slice_fun.zig |
|
const std = @import("std"); const print = std.debug.print; fn show_weapons(z_pows: []const u8) void { const n_what: u8 = z_pows[0]; const s_weapon = switch (n_what) { 0 => "Rock", 1 => "Paper", 2 => "Scissors", 3 => "Weight Gain 3000", 4 => "I call it \"laser\"", else => "not allowed!", }; print("\t{s}\n", .{s_weapon}); if (z_pows.len > 1) { show_weapons(z_pows[1..]); } } pub fn main() void { // available weapons const a_weapon_codes = [_]u8{ 0, 1, 2, 3, 4, // ... }; // the player has this weapon skill level const n_weapons_lvl = 5; // which weapons can be used with the achieved level const z_can_use = if (n_weapons_lvl > 4) a_weapon_codes[0..] else a_weapon_codes[0..3]; print("\nAt level {} you can use these weapons:\n", .{n_weapons_lvl}); show_weapons(z_can_use); } |
|
$ zig run slice_fun.zig |
|
At level 5 you can use these weapons: Rock Paper Scissors Weight Gain 3000 I call it "laser" |
Looking at this last example from the chapter, you might be wondering: “What’s that strange function that calls itself to list one weapon after another?” It’s a recursive function. But that’s not the only way to process data in sequence. That’s what the next chapter is about.
