for Loops
A while loop repeats while a condition is true.
A for loop is different. It walks through items.
Use for when you already have a group of values and want to visit each one.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 10, 20, 30 };
for (numbers) |n| {
std.debug.print("{}\n", .{n});
}
}This prints:
10
20
30The array is:
const numbers = [_]u8{ 10, 20, 30 };The loop is:
for (numbers) |n| {
std.debug.print("{}\n", .{n});
}Read it as:
for each item in numbers, call the current item n, then run the loop body.
The Basic Shape
The basic shape is:
for (items) |item| {
// use item
}The name between | and | is the loop capture.
|item|That name refers to the current item.
You can choose a better name:
for (users) |user| {
// use user
}
for (bytes) |byte| {
// use byte
}
for (tokens) |token| {
// use token
}Good names make loops easier to read.
Looping Over an Array
An array has a fixed length known at compile time.
const letters = [_]u8{ 'a', 'b', 'c' };
for (letters) |ch| {
std.debug.print("{c}\n", .{ch});
}This prints:
a
b
cThe format string uses {c} because each item is a character.
Looping Over a Slice
A slice is a view into a sequence of items.
const values = [_]u8{ 5, 6, 7, 8 };
const slice = values[1..3];
for (slice) |value| {
std.debug.print("{}\n", .{value});
}This prints:
6
7The slice:
values[1..3]starts at index 1 and stops before index 3.
So it contains:
6, 7for works naturally with slices. This is one of the most common loop patterns in Zig.
Looping Over a String
A string literal in Zig is a sequence of bytes.
const text = "abc";
for (text) |byte| {
std.debug.print("{c}\n", .{byte});
}This prints:
a
b
cThis loops over bytes, not Unicode characters.
For plain ASCII text, each byte is one visible character. For UTF-8 text, one visible character may use more than one byte. We will study UTF-8 later. For now, remember this rule: for over a string gives bytes.
Getting the Index
Often you need both the item and its index.
Use a range beside the collection:
const std = @import("std");
pub fn main() void {
const names = [_][]const u8{ "ada", "zig", "c" };
for (names, 0..) |name, index| {
std.debug.print("{}: {s}\n", .{ index, name });
}
}This prints:
0: ada
1: zig
2: cThe loop is:
for (names, 0..) |name, index| {This means:
walk through names
also count upward starting from 0
call the current item name
call the current index index
The order matters. The captures match the inputs.
for (names, 0..) |name, index| {name comes from names.
index comes from 0...
Ranges
A range can be used by itself.
for (0..5) |i| {
std.debug.print("{}\n", .{i});
}This prints:
0
1
2
3
4The range:
0..5starts at 0 and stops before 5.
So it includes 0, 1, 2, 3, and 4.
The end is exclusive.
Start Is Included, End Is Excluded
This is important:
0..5means:
0, 1, 2, 3, 4It does not include 5.
This same rule appears in slice syntax:
items[0..5]That means items at indexes 0, 1, 2, 3, and 4.
The rule is consistent: start included, end excluded.
Modifying Items
By default, a for loop gives you each item as a value.
If you want to modify items, loop over pointers.
const std = @import("std");
pub fn main() void {
var numbers = [_]u8{ 1, 2, 3 };
for (&numbers) |*n| {
n.* += 10;
}
for (numbers) |n| {
std.debug.print("{}\n", .{n});
}
}This prints:
11
12
13The important line is:
for (&numbers) |*n| {&numbers gives access to the array by reference.
|*n| captures a pointer to each item.
Inside the loop, n is a pointer. To read or write the pointed-to value, use:
n.*So this:
n.* += 10;means:
add 10 to the actual item inside the array.
Why |*n| Matters
This loop does not change the array:
for (numbers) |n| {
var copy = n;
copy += 10;
}Here, n is a copy of each item. Changing a copy does not change the array.
This loop changes the array:
for (&numbers) |*n| {
n.* += 10;
}Here, n points to the actual item.
Use pointer capture only when you need mutation.
continue in a for Loop
continue skips the rest of the current loop body.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 1, 2, 3, 4, 5 };
for (numbers) |n| {
if (n % 2 == 0) {
continue;
}
std.debug.print("{}\n", .{n});
}
}This prints:
1
3
5When n is even, the loop skips the print statement.
break in a for Loop
break exits the loop immediately.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 10, 20, 30, 40 };
for (numbers) |n| {
if (n == 30) {
break;
}
std.debug.print("{}\n", .{n});
}
}This prints:
10
20When n becomes 30, the loop stops.
for with else
A for loop can have an else branch.
The else branch runs if the loop finishes normally.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 1, 2, 3 };
for (numbers) |n| {
std.debug.print("{}\n", .{n});
} else {
std.debug.print("done\n", .{});
}
}This prints:
1
2
3
doneIf the loop exits with break, the else branch does not run.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 1, 2, 3 };
for (numbers) |n| {
if (n == 2) {
break;
}
std.debug.print("{}\n", .{n});
} else {
std.debug.print("done\n", .{});
}
}This prints:
1The loop stopped early, so else did not run.
This is useful for search logic.
Searching with for
Suppose you want to know whether an array contains a value.
const std = @import("std");
pub fn main() void {
const numbers = [_]u8{ 3, 6, 9, 12 };
const target: u8 = 9;
const found = for (numbers) |n| {
if (n == target) {
break true;
}
} else false;
std.debug.print("found = {}\n", .{found});
}This prints:
found = trueRead it like this:
loop through the numbers
if we find the target, stop and return true
if the loop finishes without finding it, return false
This is compact, but still explicit.
Looping Over Two Collections
A for loop can walk through multiple collections at the same time.
const std = @import("std");
pub fn main() void {
const names = [_][]const u8{ "Ada", "Bob", "Cora" };
const scores = [_]u8{ 90, 75, 88 };
for (names, scores) |name, score| {
std.debug.print("{s}: {}\n", .{ name, score });
}
}This prints:
Ada: 90
Bob: 75
Cora: 88The first capture comes from the first collection.
The second capture comes from the second collection.
for (names, scores) |name, score| {name comes from names.
score comes from scores.
The collections must have compatible lengths for the loop.
Ignoring a Value
Sometimes you do not need one of the captured values.
Use _ to ignore it.
const values = [_]u8{ 10, 20, 30 };
for (values, 0..) |_, index| {
std.debug.print("index = {}\n", .{index});
}This prints:
index = 0
index = 1
index = 2The item itself is ignored. Only the index is used.
for vs while
Use for when you are walking through known items.
for (items) |item| {
// use item
}Use while when repetition depends on a condition.
while (remaining > 0) {
// keep working
}Use for for arrays, slices, strings, and ranges.
Use while for open-ended loops, retry loops, and loops that stop based on changing state.
The Main Idea
A for loop walks through items.
It is the normal Zig tool for arrays, slices, strings, and ranges.
The most important forms are:
for (items) |item| {
// use item
}for (items, 0..) |item, index| {
// use item and index
}for (&items) |*item| {
// modify item
}Keep this rule in mind: use for when you already have things to visit. Use while when you only have a condition.