Zig has no class syntax.
A function that belongs with a type is written inside the type.
const Point = struct {
x: i32,
y: i32,
pub fn moveRight(p: *Point, n: i32) void {
p.x += n;
}
};The struct has two fields and one function. The function is part of the namespace of Point.
It is called with the type name:
Point.moveRight(&point, 5);A complete program:
const std = @import("std");
const Point = struct {
x: i32,
y: i32,
pub fn moveRight(p: *Point, n: i32) void {
p.x += n;
}
};
pub fn main() void {
var point = Point{
.x = 10,
.y = 20,
};
Point.moveRight(&point, 5);
std.debug.print("x = {d}, y = {d}\n", .{ point.x, point.y });
}The output is:
x = 15, y = 20There is no hidden receiver. The first parameter is ordinary. It has the type *Point, a pointer to Point.
Zig also lets this call be written with dot syntax:
point.moveRight(5);This is the same call. Zig passes &point as the first argument because moveRight expects *Point.
const std = @import("std");
const Point = struct {
x: i32,
y: i32,
pub fn moveRight(p: *Point, n: i32) void {
p.x += n;
}
};
pub fn main() void {
var point = Point{
.x = 10,
.y = 20,
};
point.moveRight(5);
std.debug.print("x = {d}, y = {d}\n", .{ point.x, point.y });
}The output is still:
x = 15, y = 20This is method-call syntax, but the method is still just a function.
A function may take the value by copy.
const Point = struct {
x: i32,
y: i32,
pub fn sum(p: Point) i32 {
return p.x + p.y;
}
};Call it with:
const n = point.sum();The value point is passed as the first argument.
A complete program:
const std = @import("std");
const Point = struct {
x: i32,
y: i32,
pub fn sum(p: Point) i32 {
return p.x + p.y;
}
};
pub fn main() void {
const point = Point{
.x = 7,
.y = 8,
};
const n = point.sum();
std.debug.print("{d}\n", .{n});
}The output is:
15Use a value parameter when the function only needs to read the value and the value is small.
Use a pointer parameter when the function must change the value.
pub fn reset(c: *Counter) void {
c.value = 0;
}Use a pointer to constant when the function reads a large value without copying it.
pub fn area(r: *const Rectangle) u32 {
return r.width * r.height;
}This says that the function receives an address, but it will not modify the rectangle through that address.
const Rectangle = struct {
width: u32,
height: u32,
pub fn area(r: *const Rectangle) u32 {
return r.width * r.height;
}
};Call it in the same way:
const a = rect.area();Zig chooses the correct first argument form from the function parameter type.
If the first parameter is Rectangle, the value is passed.
If the first parameter is *Rectangle, a pointer is passed.
If the first parameter is *const Rectangle, a pointer to constant is passed.
This makes the call site short while keeping the function signature explicit.
A common style is to name the first parameter self.
const Counter = struct {
value: u32,
pub fn inc(self: *Counter) void {
self.value += 1;
}
pub fn get(self: Counter) u32 {
return self.value;
}
};self is only a parameter name. It has no special meaning in the language.
A complete counter:
const std = @import("std");
const Counter = struct {
value: u32,
pub fn inc(self: *Counter) void {
self.value += 1;
}
pub fn add(self: *Counter, n: u32) void {
self.value += n;
}
pub fn get(self: Counter) u32 {
return self.value;
}
};
pub fn main() void {
var counter = Counter{
.value = 0,
};
counter.inc();
counter.add(4);
std.debug.print("{d}\n", .{counter.get()});
}The output is:
5The functions are ordinary declarations. They may be called either way:
counter.inc();
Counter.inc(&counter);Both mean the same thing.
Functions inside structs may also be constructors.
const Point = struct {
x: i32,
y: i32,
pub fn init(x: i32, y: i32) Point {
return Point{
.x = x,
.y = y,
};
}
};Call it with the type name:
const p = Point.init(10, 20);init is only a name. Zig does not treat it specially. It is a convention.
A type may have several constructor-like functions.
const Point = struct {
x: i32,
y: i32,
pub fn init(x: i32, y: i32) Point {
return .{
.x = x,
.y = y,
};
}
pub fn zero() Point {
return .{
.x = 0,
.y = 0,
};
}
};Inside a function that returns Point, the literal may use .{ ... } because the return type is known.
return .{
.x = 0,
.y = 0,
};This is the same as writing:
return Point{
.x = 0,
.y = 0,
};Methods are useful because they keep operations near the data they operate on. They do not add inheritance, virtual dispatch, or hidden fields. They are namespaced functions with a convenient call form.
Exercises.
7-9. Add a method moveUp to Point that adds to y.
7-10. Define a Rectangle struct with width and height. Add an area method.
7-11. Define a Counter struct with inc, dec, and reset methods.
7-12. Write a constructor function Point.init(x, y) and use it in main.