🎓 A1Learner
🔍
0 of 46 lessons completed

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?