Lesson 7 / 25

Structs, Methods and Namespaces

Define structs with default values, methods and declarations.

Structs are also namespaces

A struct groups fields: const Item = struct { sku: []const u8, qty: u32 = 1 };, with optional default values. Instances are created with struct literals (.{ .sku = "pen", .qty = 2 }), where the leading dot lets the compiler infer the type from context. Functions declared inside a struct are methods when their first parameter is the struct type or a pointer to it, and can be called with dot syntax: item.total() is sugar for Item.total(item). Use self: Item (a copy, read-only) or self: *Item (to modify). @This() refers to the enclosing type, useful in generic code. Structs also hold declarations (constants, nested types, functions), so a struct doubles as a namespace, and every Zig source file is itself a struct whose public (pub) declarations can be imported with @import. A common convention is an init function returning a configured instance and a deinit method that releases resources. Anonymous struct literals and tuples (.{ 1, "two" }) are used for format arguments and multiple return values.

A struct with fields and declarations

Fields hold per-instance data; declarations such as methods and constants belong to the type.

A box split into an upper area with field slots and a lower area with function and constant icons, labelled as shared by all instances.
Figure 3.1 — Fields and declarations in a struct.

An invoice struct with methods

Default values, methods with self and a namespaced constant.

const std = @import("std");

const LineItem = struct {
    sku: []const u8,
    price_paise: u32,
    qty: u32 = 1,                                  // default value

    fn total(self: LineItem) u64 {
        return @as(u64, self.price_paise) * self.qty;
    }
};

const Invoice = struct {
    number: u32,
    items: []const LineItem,
    paid: bool = false,

    pub const gst_percent: u64 = 18;               // a declaration, shared by all invoices

    pub fn subtotal(self: Invoice) u64 {
        var sum: u64 = 0;
        for (self.items) |item| sum += item.total();
        return sum;
    }

    pub fn grandTotal(self: Invoice) u64 {
        const s = self.subtotal();
        return s + (s * gst_percent + 50) / 100;   // round to nearest paisa
    }

    pub fn markPaid(self: *Invoice) void {         // pointer receiver: modifies the invoice
        self.paid = true;
    }
};

pub fn main() void {
    const items = [_]LineItem{
        .{ .sku = "pen", .price_paise = 4_950, .qty = 2 },
        .{ .sku = "ink", .price_paise = 19_900 },   // qty defaults to 1
    };
    var inv = Invoice{ .number = 7, .items = &items };
    inv.markPaid();
    std.debug.print("INV-{d}: subtotal {d}, total {d}, paid {}\n", .{ inv.number, inv.subtotal(), inv.grandTotal(), inv.paid });
}

Files are structs

Because each file is a struct, const invoice = @import("invoice.zig"); gives you a namespace whose pub declarations you can use as invoice.Invoice or invoice.parse. Non-pub declarations stay private to the file.

Quick check: What does `inv.markPaid()` desugar to when markPaid takes `self: *Invoice`?

  • Invoice.markPaid(&inv), passing a pointer
  • Invoice.markPaid(inv) with a copy
  • A virtual method call
  • It is a compile error
Answer

Invoice.markPaid(&inv), passing a pointer — Method call syntax passes the address automatically when the parameter is a pointer.