Captures in Zig provide the ability to unpack or catch values as an extension of other language features. Capture syntax provide an elegant way to reduce boilerplate and clearly express intent with a minimal syntax.
Data can be captured via a pointer or direct copy. To see common issues surrounding this, see: Unintentional Copy On Capture
if
With booleans (no captures here):
if (true) return x;
if (a > b) x = a else x = b;
With optionals (capture payload if not null):
if (maybe_byte) |byte| x = byte else x = 0;
if (maybe_byte == null) return;
With error unions (capture payload on success, or the error value on error):
if (canFail()) |result| {
x = result;
} else |err| {
print("error: {}\n", .{err});
return err;
}
This can be combined with switch:
if (canFail()) |result| {
x = result;
} else |err| switch(err) {
error.SpecificFailure => {
// handle specific failure
},
else => |leftover_err| return leftover_err,
}
while
With booleans (no capture):
while (i < n) : (i += 1) { ... }
With optionals (capture the payload if not null; break loop if null):
while (maybe_byte) |byte| { ... }
With error unions (capture payload on success, else clause is mandatory here):
while (canFail()) |result| { ... } else |err| { ... }
for
for
loops only work on arrays, slices, and ranges. The payload is always the current iteration item. You can iterate over multiple objects simultaneously.
for (list) |item| { ... }
for (list_a, 0..) |item, i| { ... }
for (list_a, list_b, list_c) |a, b, c| { ... }
switch
When switching on a tagged union, the prongs can capture the payload if any. Also when a prong expression is a range or a list of expressions, you can capture the actual matching value.
switch (my_tagged_union) {
.a => |a| ...,
.b => ...,
.c => |c| ...,
else => |value| ...,
}
By using an inline else prong with a tagged union you can also capture the tag.
switch (my_tagged_union) {
.a => |a| ...,
.b => ...,
.c => |c| ...,
inline else => |value, tag| ...,
}
In addition, when switching on a non-exhaustive enum you can match on _
prong instead of else
, making sure at compile-time that the switch is exhaustive, i.e. uses all known enum tags.
switch (x) {
0...9 => |n| ...,
13, 19, 23 => |n| ...,
else => ...,
}
switch (my_non_exhaustive_enum) {
.a, .b => |a_b| ...,
.c => |c| ...,
_ => ...,
}
When switching on an error set the payload of the else prong captures the smaller error set of the remaining possible errors:
switch(errorset) {
error.SpecificFailure => {
// handle specific failure
},
else => |leftover_err| return leftover_err,
}
catch
The catch-capture works with error unions as a trailing syntax. Expressions that result in an error can be caught and the payload can be captured for further handling.
// Reserve memory for a type T. If the create statement succeeds
// the variable ptr will be of type *T. If it fails, the error
// returned by create is captured and can be further handled
// in the catch block.
const ptr = allocator.create(T) catch |e| {
// handling code based on the captured error "e"
};
errdefer
The errdefer-capture allows capturing the error to be used in the deferred block:
errdefer |err| std.log.err("failed to read the port number: {!}", .{err});