Enums
Enums let us define data types with a limited set of values, each one given a name.
const EnumName = enum {
value_name,
…
value_name_N,
};
In the case of a motor, freshly disassembled from a drone and connected to our measuring instruments, we could define two main states: on and off.
enum_e_status.zig |
|
const std = @import("std"); const print = std.debug.print; const EngineStatus = enum { on, off, }; pub fn main() void { const e_status_1 = EngineStatus.off; print("Motor state 1: {}\n", .{e_status_1}); } |
|
$ zig run enum_e_status.zig |
|
Motor state 1: .off |
If we only have these two states, a single bit would be enough to represent them. We can specify the integer type used to store the enum value:
|
const EngineStatus = enum(u1) { on, off, }; |
If we define the integer type, we need to consider its range:
|
const EngineStatus = enum(u1) { on, off, unknown, // <- this one no longer fits in a single bit }; |
error: enumeration value '2' too large for type 'u1'
The ordinal values of enums start at 0. We can access them using:
|
@intFromEnum(EngineStatus.off) // -> 1 |
We can also define the values explicitly:
|
const EngineStatus = enum(i8) { // i8 to allow negative values on = 1, off = 0, failure = -1, }; |
If we don't explicitly define an ordinal value after having manually assigned others, Zig will try to assign it based on the last defined value:
|
const EngineStatus = enum(i8) { on = 1, off = 0, failure = -1, testing, // added without manual assignment }; |
This will cause an error because the compiler will try to assign testing the value 0, which is already taken by off:
error: enum tag value 0 already taken
testing,
^~~~~~~
To fix it, we just need to put the definitions in the correct order:
|
// manual values in order const EngineStatus = enum(i8) { failure = -1, off = 0, on = 1, testing,// added without manual assignment }; |
Non-exhaustive enums
Check out this way of building an enum:
|
const EngineStatus = enum(i8) { <- tag type is mandatory off = 0, on = 1, failure = -1, _, // <- marks it as non-exhaustive }; |
Until now, we've always declared exhaustive enums, where all possible values are defined. But by using _ and an explicit type like i8, we've created a non-exhaustive enum.
enum_non_exhaustive.zig |
|
const std = @import("std"); const print = std.debug.print; const EngineStatus = enum(u8) { // -> tag type is mandatory off, on, failure, _, // <- marks it as non-exhaustive }; pub fn main() void { // normal value with a declared tag const e_1: EngineStatus = .on; // legal, even 33 isn't a declared tag const e_2: EngineStatus = @enumFromInt(33); print( "status 1 = {}, int = {}\n", .{ e_1, @intFromEnum(e_1) }, ); print( "status 2 = {}, int = {}\n", .{ e_2, @intFromEnum(e_2) }, ); // cover values with else switch (e_2) { .off => print("engine off\n", .{}), .on => print("engine on\n", .{}), // .failure => print("engine failure\n", .{}), // if we omit .failure, _ => isn’t enough, // we need else else => |n_val| print("other value: {}\n", .{n_val}), // if you only include only _ you’ll get an error // because .failure is declared but not covered _ => |n_val| print("undeclared value: {}\n", .{n_val}), } } |
|
$ zig run enum_nonexhaustive.zig |
|
status 1 = .on, int = 1 status 2 = @enumFromInt(33), int = 33 undeclared value: @enumFromInt(33) |
Note the possible range of u8: from 0 to 255. Now that the enum is non-exhaustive, Zig can no longer assume we've covered the entire range. If we do a switch on this type, Zig requires us to include either an else (for uncovered values), or _ (untagged values), or both.
|
// cover values with else switch (e_status) { .off => ..., .on => ..., // if we omit one, _ => isn’t enough, // we need else else => |n_val| ... // else is mandatory _ => |n_val| ... // only undeclared values } |
It's also interesting that we can force an undeclared value like this:
|
const e_status_x: EngineStatus = @enumFromInt(33); // legal, even 33 isn't a declared tag |
This is useful if, for example, the value you're converting comes from a file, but it also means you have to be very careful when validating it. At first, non-exhaustive enums might be a bit advanced for a beginner in Zig, so we'll dive into them more in upcoming books in the series.
Variables and constants inside the enum
We can declare variables and constants inside the enum. These belong to the type itself; their scope will be global, and their value won’t be tied to an instance.
enum_2.zig |
|
const std = @import("std"); const print = std.debug.print; const EngineStatus = enum(u2) { // this variable gives a value to the type // not to individual instances var n_starts: u32 = 0; on, off, failure, }; pub fn main() void { const e_status1 = EngineStatus.on; // global access, modifies the type’s data EngineStatus.n_starts += 1; var e_status2 = EngineStatus.on; // global access EngineStatus.n_starts += 1; print("Motor state 1: {}\n", .{e_status1}); e_status2 = EngineStatus.off; print("Motor state 2: {}\n", .{e_status2}); print("Motor start counter: {}\n", .{EngineStatus.n_starts}); } |
|
$ zig run enum_2.zig |
|
Motor state 1: .on Motor state 2: .off Motor start counter: 2 |
Enum methods
We can define functions inside an enum. When a function is part of an enum, struct, or union, it's called a method. These don’t depend on a specific instance unless they receive one as a parameter (self).
enum_6engines.zig |
|
const std = @import("std"); const print = std.debug.print; const EngineStatus = enum(u3) { var n_can_start: u32 = 0; // EngineStatus type data failure, off, on, autorepairing, // method for a specific instance (receives self) fn can_start(self: EngineStatus) bool { return switch (self) { .autorepairing, .failure => false, else => true, }; } // method of the type (does not receive self) fn count_can_start() void { EngineStatus.n_can_start += 1; } }; pub fn main() void { const a_engines = [_]EngineStatus{ .off, .off, .on, .autorepairing, .off, .failure, }; for (a_engines) |e_status| { if (e_status.can_start()) EngineStatus.count_can_start(); print("{}\n", .{e_status}); } print("There are {} engines available.\n", .{EngineStatus.n_can_start}); } |
|
$ zig run enum_6engines.zig |
|
.off .off .on .autorepairing .off .failure There are 4 engines available. |
Useful functions for working with enums:
- std.meta.stringToEnum converts a string to its matching enum value
- @tagName converts an enum to a string
- @intFromEnum converts an enum to an integer
- @enumFromInt converts an integer to an enum
- std.meta.tags returns the enum values as an array
- std.meta.fields returns info about the enum
enum_values.zig |
|
const std = @import("std"); const print = std.debug.print; const EngineStatus = enum(i8) { failure = -1, off = 0, on = 1, testing, // 2 autorepairing, // 3 }; pub fn main() void { // std.meta.fields - field information print("\nNumber of states: {any}\n", .{std.meta.fields(EngineStatus).len}); // std.meta.tags - all enum values as an array print("All detected possible states:\n", .{}); for (std.meta.tags(EngineStatus)) |e_status| { print(" {} - \"{s}\" = {d}\n", // state .{ e_status, @tagName(e_status), @intFromEnum(e_status), }); } } |
|
$ zig run enum_values.zig |
|
Number of states: 5 All detected possible states: .failure - "failure" = -1 .off - "off" = 0 .on - "on" = 1 .testing - "testing" = 2 .autorepairing - "autorepairing" = 3 |