Zig Union Safety
Union safety is the rule that code reads only the field a union currently holds. The tag tells code which field that is.
Check before reading
A tag check proves the field before direct access. Only then is the dot read safe.
const std = @import("std");
const Value = union(enum) {
int: i32,
float: f32,
text: []const u8,
};
pub fn main() !void {
const out = std.io.getStdOut().writer();
const v = Value{ .float = 2.5 };
if (v == .float) {
try out.print("float payload={d}\n", .{v.float});
}
}Safe fallback
The else branch covers every non-int field. Code that needs an int gets a safe fallback instead.
const std = @import("std");
const Value = union(enum) {
int: i32,
float: f32,
text: []const u8,
};
fn intOrZero(v: Value) i32 {
switch (v) {
.int => |n| return n,
else => return 0,
}
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const a = Value{ .int = 42 };
const b = Value{ .float = 2.5 };
try out.print("a={d} b={d}\n", .{ intOrZero(a), intOrZero(b) });
}Tag after reassignment
Reassignment changes the tag with the payload. Reading the tag again shows the new field.
const std = @import("std");
const Value = union(enum) {
int: i32,
float: f32,
text: []const u8,
};
fn describe(v: Value) []const u8 {
switch (v) {
.int => return "int",
.float => return "float",
.text => return "text",
}
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
var v = Value{ .int = 42 };
try out.print("first={s}\n", .{describe(v)});
v = Value{ .float = 2.5 };
try out.print("second={s} tag={s}\n", .{ describe(v), @tagName(v) });
}Example
const std = @import("std");
const Value = union(enum) {
int: i32,
float: f32,
text: []const u8,
};
fn readInt(v: Value) i32 {
switch (v) {
.int => |n| return n,
else => return -1,
}
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const a = Value{ .int = 99 };
const b = Value{ .text = "hi" };
try out.print("a={d} b={d}\n", .{ readInt(a), readInt(b) });
}Key points
- Active field: an active field is the field a union currently holds.
- Inactive field: an inactive field is any field that is not the active field.
- Tag check: a tag check is a comparison against a tag, like v == .float, that proves which field is stored before code reads it.
- A switch branch runs only when its field is the stored field, so its payload is safe to use.
- Else branch: an else branch is a switch branch that handles all fields not named before it.
- Direct access: direct access is reading a field with a dot, like v.int, and it is safe only after a tag check proves that field is stored.
The function switches on the tag. The int branch returns its payload. Every other field falls back to -1, so no inactive field is ever read.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A tagged union holds .text = "hi". Which read is safe?