If Expressions
In Zig, if is both a control-flow statement and an expression that can
produce a value. Unlike C, JavaScript and most languages you have used, Zig
has no truthy or falsy values: the condition must be a bool and nothing else
converts to one.
const std = @import("std");
const expect = std.testing.expect;
test "if as a statement" {
const a = true;
var x: u16 = 0;
if (a) {
x += 1;
} else {
x += 2;
}
try expect(x == 1);
}
test "if as an expression" {
const a = true;
// The ternary equivalent; both branches must yield the same type.
const x: u16 = if (a) 1 else 2;
try expect(x == 1);
}
test "if unwraps an optional" {
const maybe: ?u8 = 42;
const doubled: u16 = if (maybe) |value| value * 2 else 0;
try expect(doubled == 84);
}There is no truthiness in Zig. if (1) does not compile; neither does
if (pointer). The condition must be a bool:
error: expected type 'bool', found 'u8'
That removes an entire family of bugs where a value is accidentally treated as
a flag. In C, if (strcmp(a, b)) is true when the strings differ, if (n = 0)
is an assignment that is always false, and if (ptr) and if (*ptr) are one
character apart with completely different meanings. None of those parse here.
if produces a value
Zig has no ternary operator because it does not need one:
const x: u16 = if (a) 1 else 2;
Both branches must agree on a type. Used as an expression, the else is
mandatory, and the error says so in terms of what is missing rather than what
is absent:
error: expected type 'u16', found 'void'
An if with no else has nothing to evaluate to when the condition is false,
so its type is void. Read that message as “this expression does not always
produce a value”.
The one case that slips through is a condition the compiler already knows. If
the condition is comptime-known, only the taken branch is compiled, so
const x: u16 = if (true) 1; builds. Make the condition a runtime value and it
stops. That is not a special rule for if; it is the same constant folding that
lets comptime code disappear entirely.
Unwrapping
if is also how you get inside an optional, using payload capture:
if (maybe) |value| {
// value is the payload, not the optional
} else {
// maybe was null
}
The captured value is a copy. To change the thing inside the optional, capture
by pointer with |*value| and write through it. Without the * the mutation
applies to the copy and is lost, which is worth knowing because nothing warns
you.
Error unions use the same shape, with the error going to the else:
if (parse(text)) |n| {
use(n);
} else |err| {
report(err);
}
This is a recurring theme: the check and the unwrap are one construct, so they cannot fall out of sync. There is no way to test for null and then read the payload through a different path that forgot the test.
When to use something shorter
if on an optional is the general form, and two shorthands cover the common
cases:
maybe orelse defaultwhen you only want a fallback value.try f()when you only want to propagate the error upward.f() catch |err| ...when you want to handle it here.
Reach for if when both sides do real work. Reach for the shorthands when one
side is a single expression, because x orelse 0 says what
if (x) |v| v else 0 says in a quarter of the space. Optionals are covered in
optionals and the error forms in
errors.