Skip to content

Class Basics

A simplest class

zig
pub const Basic = struct {
    pub usingnamespace zoop.Fn(@This());
    mixin: zoop.Mixin(@This()),
}

A simplest class that needs to use two zoop functions:

  • zoop.Fn: Import all for the class:
  • Basic Method
  • Methods defined by the class through its own Fn function
  • Methods inherited from parent classes
  • zoop.Mixin: Defines a required mixin field for the class, which contains type interface information and parent class fields. The field name mixin is a keyword, no other name can be used. The field does not have to be the first field.

Note

The mixin field is maintained by zoop, so users should not modify it.

Add methods and fields to the class

Next, we add a msg field to Basic, and two inherited methods getMsg() and print():

zig
pub const Basic = struct {
    pub usingnamespace zoop.Fn(@This());

    msg: []const u8,
    mixin: zoop.Mixin(@This()),

    pub fn init(self: *Basic, msg: []const u8) void {
        self.msg = msg;
    }

    pub fn Fn(comptime T: type) type {
        return zoop.Method(.{
            struct {
                pub fn getMsg(this: *T) []const u8 { 
                    return this.cast(Basic).msg;
                }
            },
            struct {
                pub fn print(this: *T) void { 
                    std.debug.print("{s}\n", .{this.cast(Basic).msg});
                }
            },
        });
    }
}

There are two types of class methods:

  • Inheritable methods: methods defined in the Fn function, such as the getMsg() and print() methods above
  • Non-inheritable methods: methods defined outside of a Fn function, such as the init() method above

If you don't understand line 15 now, you can take a look at Type Conversion in advance. Next, we will explain in detail how methods and fields are selectively inherited by subclasses.

Class inheritance

We introduced a new keyword extends for class inheritance. The following is a definition of a class SubClass that inherits the above Basic, and adds an inheritable method setMsg. Inheritance is achieved through the highlighted line:

zig
pub const SubClass = struct {
    pub const extends = .{Basic};
    pub usingnamespace zoop.Fn(@This());

    mixin: zoop.Mixin(@This()),

    pub fn init(self: *SubClass, msg: []const u8) void {
        self.cast(Basic).init(msg);
    }

    pub fn Fn(comptime T: type) type {
        return zoop.Method(.{
            struct {
                pub fn setMsg(this: *T, msg: []const u8) void { 
                    this.cast(Basic).msg = msg;
                }
            },
        });
    }
}

To make it easier to understand, we list the code above after the code is expanded by comptime, assuming that the Basic class is defined in the modname module:

zig
pub const SubClass = struct {
    pub const extends = .{Basic};

    mixin: struct {
        deallocator: ?std.mem.Allocator = null,
        meta: ?MetaInfo = null,
        data: struct {
            modname_Basic: Basic,
        },
    },

    pub fn init(self: *SubClass, msg: []const u8) void {
        self.cast(Basic).init(msg);
    }

    pub fn print(this: *SubClass) void { 
        std.debug.print("{s}\n", .{this.cast(Basic).msg});
    }

    pub fn getMsg(this: *SubClass) []const u8 { 
        return this.cast(Basic).msg;
    }

    pub fn setMsg(this: *SubClass, msg: []const u8) void { 
        this.cast(Basic).msg = msg;
    }
}
  • Line 8 inherits the fields of Basic, which is obtained through zoop.Mixin
  • Line 16 inherits the Basic.print() method, which is obtained through zoop.Fn
  • Line 20 inherits the Basic.getMsg() method, which is obtained through zoop.Fn
  • Line 24 expands the newly added method setMsg() of SubClass, which is also obtained through zoop.Fn

Note

Diamond inheritance is not supported. For example, in the following code, both B and C inherit from A, so Wrong cannot inherit from both B and C:

zig
pub const A = struct {...};
pub const B = struct { pub const extends = {A}; ...};
pub const C = struct { pub const extends = {A}; ...};

// compile error!
pub const Wrong = struct { pub const extends = .{B, C}; ...};

Object creation and destruction

There are two ways to create class objects

  • Create an instance of a class on the heap: through the class's new() method
  • Create an instance of a class on the stack: through the class's make() method

Create and initialize SubClass on the heap:

zig
var sub: *SubClass = try SubClass.new(allocator);
sub.init("Hello World");

Create and initialize SubClass on the stack:

zig
var sub: SubClass = SubClass.make();
sub.initMixin();
sub.init("Hello World");

Note

Objects created on the stack must call the initMixin() method to initialize the Mixin data after creation. This is to allow rootptr to point to the object on the stack, which cannot be done in make().

Whether objects are on the heap or on the stack, they are destroyed through the destroy() method

zig
var heap_sub: *SubClass = try SubClass.new(allocator);
sub.init("Hello World");

var stack_sub: SubClass = SubClass.make();
sub.initMixin();
sub.init("Hello World");

heap_sub.destroy();
stack_sub.destroy();

If there is something that needs to be done before the object is destroyed (destructor), it can be implemented by defining a non-inheritable method deinit():

zig
pub const SubClass = struct {
    ...

    pub fn deinit(self: *SubClass) void {
        // Operations before destruction...
    }
}

destroy() will automatically call all deinit() methods of the parent class and its subclasses in order from child to parent.

Note

  • Objects on the heap must call destroy(), otherwise there will be memory leaks. Objects on the stack also need to call destroy() if they need to ensure that deinit() is called.
  • Do not destroy mixin data in the class's deinit() method, such as self.* = undefined, because the subclass's mixin contains all the fields of the parent class.

Released under the MIT License