Optionals
A ?T holds either a T or null. The compiler will not let you use the
value without handling the null case first, so there is no such thing as an
accidental null dereference.
These are real tests. Run executes the actual zig test runner, compiled to
WebAssembly.
const std = @import("std");
const expect = std.testing.expect;
test "optional payload capture" {
var maybe: ?i32 = null;
try expect(maybe == null);
maybe = 42;
if (maybe) |value| {
try expect(value == 42);
} else {
unreachable;
}
}
test "orelse supplies a default" {
const nothing: ?u32 = null;
try expect((nothing orelse 7) == 7);
}
test "optional pointers are free" {
// A `?*T` is the same size as a `*T`; null is the 0 address.
try expect(@sizeOf(?*i32) == @sizeOf(*i32));
}Payload capture
The if (maybe) |value| form is payload capture: inside the block, value is
a plain i32, not an optional. The unwrapping and the null check are the same
piece of syntax, so they cannot drift apart.
orelse is the shorthand when all you want is a fallback:
const port = maybe_port orelse 8080;
Optional pointers are free
?*T is the same size as *T. Zig uses the null address as the null tag, so
optional pointers cost nothing: no extra byte, no wrapper struct. The last test
above asserts exactly this with @sizeOf.
Note that this only applies to pointers. A ?u32 genuinely needs a tag byte
alongside the payload, because every u32 bit pattern is a valid u32.