Structs
const std = @import("std");
const expect = std.testing.expect;
const Vec3 = struct {
x: f32 = 0, // default value
y: f32 = 0,
z: f32 = 0,
pub fn dot(a: Vec3, b: Vec3) f32 {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
// Taking `*Vec3` lets the method mutate the receiver.
pub fn scale(self: *Vec3, factor: f32) void {
self.x *= factor;
self.y *= factor;
self.z *= factor;
}
};
test "construct and read" {
const v = Vec3{ .x = 1, .y = 2, .z = 3 };
try expect(v.y == 2);
}
test "defaults fill in the rest" {
const v = Vec3{ .x = 5 };
try expect(v.y == 0 and v.z == 0);
}
test "methods" {
const a = Vec3{ .x = 1, .y = 2, .z = 3 };
const b = Vec3{ .x = 4, .y = 5, .z = 6 };
try expect(Vec3.dot(a, b) == 32);
try expect(a.dot(b) == 32); // same call, method syntax
}
test "mutating methods need a mutable receiver" {
var v = Vec3{ .x = 1, .y = 1, .z = 1 };
v.scale(3);
try expect(v.x == 3);
}
test "field order is not guaranteed" {
// Zig may reorder fields for packing unless you say otherwise with
// `extern struct` (C ABI) or `packed struct` (bit-level layout).
try expect(@sizeOf(Vec3) == 12);
}Defaults
Fields may declare a default, so a literal only mentions what differs:
const v = Vec3{ .x = 5 }; // y and z default to 0
A field with no default must be supplied; there is no zero-initialisation by fiat.
Methods and the receiver
A method taking self: Vec3 gets a copy; one taking self: *Vec3 can mutate
the original. Calling a *Vec3 method requires a mutable value: v must be a
var, and Zig takes the address for you.
Choose the receiver deliberately: Vec3 for small value types you want copied,
*const Vec3 for large ones you only read, *Vec3 to mutate.
Layout is not guaranteed
A plain struct may have its fields reordered and padded however the compiler
sees fit. When the layout matters, say so:
| Declaration | Layout |
|---|---|
struct | unspecified; optimiser’s choice |
extern struct | C ABI compatible |
packed struct | bit-level, well-defined, backed by an integer |
Reaching for extern or packed “just in case” costs you the packing the
optimiser would otherwise do, so use them only at real boundaries.