ざっくりZig - structは構造型、型はtype、typeは値

2024/07/01に公開
1

struct {...}構造型(Struct Type)といい、基本的には名前と値の組であるフィールド(Field)をいくつか持ち、そのすべてが値を持ちます。デフォルトはそれぞれのフィールドを別々のメモリ領域で持ちますが、同じメモリ領域に複数のフィールドを持つこともできます。

引数の型を得られる@TypeOf関数[1]@TypeOf(struct{})の結果はtypeです。これは@TypeOf(u8)などと同じです。つまりstruct {...}u8などと同様に型であることがわかります。Zigにおいて型はtype型の値なので、変数に代入したり、関数の引数にしたり、関数の最後にreturnで返したりできます。

構造型の定義

構造型はstruct { フィールド名: 型名, ... }で定義します。x, yという2つのフィールドを持つPoint型を定義するとこうなります。

structの例
const Point = struct {  // Point型
    x: i8,  // フィールド
    y: i8,  // フィールド
};

const stdout = std.io.getStdOut().writer();
try stdout.print("@TypeOf(Point) = {}\n", .{@TypeOf(Point)});   // @TypeOf(Point) = type

値は structの型名{ .フィールド名 = 値, ...}で定義します。Point型は2つのフィールドのそれぞれが値を持つことでPoint型の値となります。

structの値
const p1 = Point{ .x = 3, .y = 5 };     // すべてのフィールドが値を持つ

各フィールドの値は変数.フィールド名で表します。構造型の値がvarで代入されていればフィールドの値を変更できます。

structフィールドの値
var p2: Point = .{ .x = 4, .y = 2 };
try stdout.print("p2.x = {}, p2.y = {}\n", .{ p2.x, p2.y });
p2.x = 1;   // varで代入されていれば変更も可
p2.y = 6;
try stdout.print("p2.x = {}, p2.y = {}\n", .{ p2.x, p2.y });
結果
p2.x = 4, p2.y = 2
p2.x = 1, p2.y = 6

フィールドにデフォルト値を設定するときは型名の後に= デフォルト値を追加します。構造型の値を定義する際にフィールドが省略されるときは、その値を自動的に設定します。

structのデフォルト値
const Point = struct {
    x: i8 = 0,
    y: i8 = 0,
};

const p3 = Point{ .x = 3 };     // .y = 0 省略
try stdout.print("p3.x = {}, p3.y = {}\n", .{ p3.x, p3.y });
const p4 = Point{ .y = 2 };     // .x = 0 省略
try stdout.print("p4.x = {}, p4.y = {}\n", .{ p4.x, p4.y });
結果
p3.x = 3, p3.y = 0
p4.x = 0, p4.y = 2

構造型に関数を定義

構造型にはフィールド値を利用した処理を行う関数を定義できます。関数には、型に属するメソッド(method)と値に属するメンバ関数(member function)があります。メンバ関数は値が変更されるかどうかで引数が異なります。

メソッド

メソッドは、値の初期化や使用していたメモリ領域の開放など、同じ型であればどんな値にでも適用できる関数です。もしPoint型に値を初期化するinitメソッドを定義すると、Point.init(...)は実行できますがPoint型の変数名.init(...)は実行できません。ちなみにinitの戻り値の型@ThisPoint型自身を表しています。

structのメソッド
const Point = struct {

    // ..... (snip) .....

    fn init(x: i8, y: i8) @This() {     // メソッド(初期化), @This()は自身の型を表す
        return .{ .x = x, .y = y };
    }
};

var p5 = Point.init(3, 4);  // p5 = .{ .x = 3, .y = 4 }
// p5.init(3, 4); 実行不可

値を変更しないメンバ関数

値を変更しないメンバ関数は、第1引数にこの関数を実行しようとする値を表す@This()型の仮引数を定義します。これをselfとすると、フィールドの値をself.フィールド名で参照できます。関数の実行時に設定する仮引数は第2引数以降に宣言します。

Point型に値の文字列表現を返すformatと2つのフィールドの値が等しいかを確認するeqlという2つのメンバ関数を定義してみます。これらを実行するときはそれぞれ変数名.format(&バッファ)あるいは変数名.eql(別のPoint型の値)となります。

structの値を変更しないメンバ関数を追加
const Point = struct {

    // ..... (snip) .....

    fn format(self: @This(), buf: []u8) ![]u8 { // 値の文字列表現を返す
        return try std.fmt.bufPrint(buf, "Point(x = {}, y = {})", .{ self.x, self.y });
    }

    fn eql(self: @This(), other: @This()) bool {
        return self.x == other.x and self.y == other.y;
    }
};

// ..... (snip) .....

var pnt_buf = [_]u8{0} ** 26;
try stdout.print("p5 = {s}\n", .{try p5.format(&pnt_buf)});
const p6 = Point.init(2, 5);
try stdout.print("p5.eql(p6) = {}\n", .{p5.eql(p6)});
結果
p5 = Point(x = 3, y = 4)
p5.eql(p6) = false

値を変更するメンバ関数

値を変更するメンバ関数は第1引数を*@This()型にします。これをselfとすると、フィールドの値をself.フィールド名で参照あるいは変更できます。関数の実行時に設定する仮引数は第2引数以降に宣言するのは値を変更しないメンバ関数と同じです。

Point型に2つのフィールドの値を引数の値と加算して変更するaddというメンバ関数を定義してみます。実行するときは変数名.add(xの値, yの値)とします。

値を変更するメンバ関数を追加
const Point = struct {

    // ..... (snip) .....

    fn add(self: *@This(), other: @This()) void {  // 値を変更するメンバ関数
        self.x += other.x;
        self.y += other.y;
    }
};

// ..... (snip) .....

p5.add(p6);
try stdout.print("p5.x = {}, p5.y = {}\n", .{p5.x, p5.y});
結果
p5.x = 5, p5.y = 9

構造型に変数を定義

構造型にはフィールドとは別に変数を定義できます。これは型名.変数名でアクセス可能で、変数がvarで設定されていれば変更もできます。この値は同じ構造型のすべての値が影響を受けます。

まずPoint.initを実行するごとに1ずつ増加させるcountを定義してみます。そしてPoint.countで値を参照してみます。

構造型に変数を定義
const Point = struct {

    // ..... (snip) .....

    var count: usize = 0;   // 作成されたPoint型の値の数

    fn init(x: i8, y: i8) @This() {
        count += 1;     // countを増加させる
        return .{ .x = x, .y = y };
    }

    // ..... (snip) .....

};

// ..... (snip) .....

try stdout.print("Point.count = {d}\n", .{Point.count});
結果
Point.count = 2

ただし、このcountの値は外部からも値を変更できますので、このままではこうした用途には向きません。これを実現するにはPointの定義を後述する@importで取り込むようにします。

構造体に定義した変数が同じ型のすべての値に影響する場合があることを示す例として、Pointに原点の位置を表すoriginを追加し、distanceで実際の位置を持つ値を算出できるようにしてみます。関数内で変数にアクセスするときself.は必要ありません。

変数が同じ型の値に影響を与える
const Point = struct {

    // ..... (snip) .....

    var origin: @This() = .{ .x = 0, .y = 0 };

    fn distance(self: @This()) @This() {
        return .{ .x = self.x - origin.x, .y = self.y - origin.y };
    }

    // ..... (snip) .....

};

// ..... (snip) .....

var p7 = Point{ .x = 1, .y = 2 };
var p8 = Point{ .x = 3, .y = 4 };
try stdout.print("p7 = {s}\n", .{try p7.distance().format(&pnt_buf)});
try stdout.print("p8 = {s}\n", .{try p8.distance().format(&pnt_buf)});
Point.origin = .{ .x = -2, .y = -3 };   // 原点の位置を変更
try stdout.print("p7 = {s}\n", .{try p7.distance().format(&pnt_buf)});
try stdout.print("p8 = {s}\n", .{try p8.distance().format(&pnt_buf)});
結果
p7 = Point(x = 1, y = 2)
p8 = Point(x = 3, y = 4)
p7 = Point(x = 3, y = 5)    // Point.originの変更により値が変化
p8 = Point(x = 5, y = 7)    // (同上)

ただし変数をconstで定義しておけば値が変更されないので、@This()Selfという変数に代入しておき、関数の第1引数の型を@This()ではなくSelfとすることがあります。

constの変数を定義
const Point = struct {

    // ..... (snip) .....

    const Self = @This();

    // ..... (snip) .....

    fn add(self: *Self, other: Self) void {
        // ..... (snip) .....
    }
};

deinit関数

Zigの標準ライブラリではinitのほかにdeinitというメンバ関数を持つ型が多く定義されています。initが値の初期化を行う関数であるのに対し、deinitは実行環境を値の使用前と同じ状態に戻す処理を行う関数です。よくあるのはアロケータ(Allocator)で確保したメモリ領域を開放する処理です。

ここではPoint型の文字列表現に必要なバッファをフィールドに追加し、アロケータでこのバッファにメモリ領域を割り当てるformatAlloc関数と、バッファを開放する関数をdeinitで定義してみます。deinitdeferによって処理の最後に実行するようにします。

deinit関数の定義
const Point = struct {

    // ..... (snip) .....

    str: [:0]u8 = undefined,    // 文字列表現のバッファ

    // ..... (snip) .....

    fn formatAlloc(self: *@This(), allocator: std.mem.Allocator) ![:0]u8 {  // バッファにメモリ領域を割り当て
        self.str = try std.fmt.allocPrintZ(allocator, "Point(x = {}, y = {})", .{ self.x, self.y });
        return self.str;
    }

    fn deinit(self: *@This(), allocator: std.mem.Allocator) void {  // バッファのメモリ領域を開放
        allocator.free(self.str);
    }

};

// ..... (snip) .....

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
defer std.debug.assert(gpa.deinit() == .ok);    // メモリリークのチェック

// Point.origin = .{ .x = 0, .y = 0 };
try stdout.print("p7 = {s}\n", .{try p7.formatAlloc(alloc)});
try stdout.print("p8 = {s}\n", .{try p8.formatAlloc(alloc)});
defer p7.deinit(alloc);     // 処理の最後に実行
defer p8.deinit(alloc);
結果
p7 = Point(x = 1, y = 2)
p8 = Point(x = 3, y = 4)

構造型のポインタとフィールド

構造型のポインタは他の型と同様ですが、フィールドの値はポインタ変数名.フィールド名もしくはポインタ変数名.*.フィールド名とします。値を変更するときも同じです。

構造型のポインタとフィールド
Point.origin = .{ .x = 0, .y = 0 };
var p9 = Point{ .x = 5, .y = 3 };
    const pp9 = &p9;
    try stdout.print("@TypeOf(pp9) = {}, pp9.* = {}\n", .{@TypeOf(pp9), pp9.*});
    try stdout.print("pp9.x = {}, pp9.*.x = {}, pp9.y = {}, pp9.*.y = {}\n", .{pp9.x, pp9.*.x, pp9.y, pp9.*.y});

    pp9.x = 2;
    pp9.y = 4;
    try stdout.print("pp9.x = {}, pp9.*.x = {}, pp9.y = {}, pp9.*.y = {}\n", .{pp9.x, pp9.*.x, pp9.y, pp9.*.y});

    pp9.*.x = 7;
    pp9.*.y = 6;
    try stdout.print("pp9.x = {}, pp9.*.x = {}, pp9.y = {}, pp9.*.y = {}\n", .{pp9.x, pp9.*.x, pp9.y, pp9.*.y});
結果
pp9.x = 5, pp9.*.x = 5, pp9.y = 3, pp9.*.y = 3
pp9.x = 2, pp9.*.x = 2, pp9.y = 4, pp9.*.y = 4
pp9.x = 7, pp9.*.x = 7, pp9.y = 6, pp9.*.y = 6

フィールドの型を任意で指定できるようにする

これまでPointのフィールドx, yはいずれもi8でした。これ以外の型も扱えるようにすれば汎用性が高まります。そうするには、フィールドの型(type)を引数とする関数を定義し、その関数からPointに相当する型を返してもらうようにします。Zigでは型がtype型の値なので、戻り値の型はtypeになります。なお、この関数にはフィールドの値を文字列化する際の書式も同時に指定できるようにしておきます。

フィールドの型とフィールドの値を文字列化する際の書式を任意に指定する関数
// 引数Tはフィールドの型、fmtはフィールドの値を文字列化する際の書式
// いずれもcomptime(コンパイル時点で内容を確定)とする
fn PointT(comptime T: type, comptime fmt: []const u8) type {
    return  struct {
        x: T,
        y: T,
        str: [:0]u8 = undefined,

        const Self = @This();
        const strFmt = "Point(x = {" ++ fmt ++ "}, y = {" ++ fmt ++ "})";

        fn init(x: T, y: T) Self {
            return .{ .x = x, .y = y };
        }

        fn deinit(self: *Self, allocator: std.mem.Allocator) void {
            allocator.free(self.str);
        }

        fn format(self: Self, buf: []u8) ![]u8 {
            return try std.fmt.bufPrint(buf, strFmt, .{ self.x, self.y });
        }

        fn formatAlloc(self: *Self, allocator: std.mem.Allocator) ![:0]u8 {
            self.str = try std.fmt.allocPrintZ(allocator, strFmt, .{ self.x, self.y });
            return self.str;
        }

        fn eql(self: Self, other: Self) bool {
            return self.x == other.x and self.y == other.y;
        }

        fn add(self: *Self, other: Self) void {
            self.x += other.x;
            self.y += other.y;
        }
    };
}
関数の実行
// フィールドの型がf16のPoint相当の型から値を生成
// init関数の引数もf16で設定
var p10 = PointT(f16, "d:0>8.3").init(12.34, 56.78);
try stdout.print("p10: {s}\n", .{try p10.formatAlloc(alloc)});
p10.deinit(alloc);
結果
p10: Point(x = 0012.340, y = 0056.780)

別の例として、任意の型を扱えるスタックを定義してみます。スタックのためのバッファはアロケータで一定の容量を確保し、その範囲内でのみデータを格納できるものとします。ポイントはinit関数でバッファを確保し、deinit関数でバッファを解放することと、データを格納するpushとデータを取得するpopのそれぞれでエラーを返す場合があることです。

アロケータでバッファを確保し任意の型を扱えるようにするスタック
fn StackAlloc(comptime T: type) type {
    return struct {
        allocator: std.mem.Allocator,
        buffer: []T,
        capacity: usize,

        const Self = @This();

        fn init(allocator: std.mem.Allocator, capacity: usize) !Self {
            return .{
                .allocator = allocator,
                .buffer = try allocator.alloc(T, capacity),
                .capacity = capacity
            };
        }

        fn deinit(self: *Self) void {
            self.allocator.free(self.buffer);
        }

        fn push(self: *Self, value: T) error{StackFull}!void {
            if (self.capacity == 0) return error.StackFull;
            self.capacity -= 1;
            self.buffer[self.capacity] = value;
        }

        fn pop(self: *Self) error{StackEmpty}!T {
            if (self.capacity == self.buffer.len) return error.StackEmpty;
            defer self.capacity += 1;
            return self.buffer[self.capacity];
        }
    };
}
スタックの実行イメージ
// u8型を扱うスタック
var stack_u8 = try StackAlloc(u8).init(alloc, 3);
defer stack_u8.deinit();
// データの格納
for(0..stack_u8.capacity) |i| {
    try stack_u8.push(@intCast(i + 1));
    try stdout.print("stack_u8.push({}): stack_u8.capacity = {}\n", .{i + 1, stack_u8.capacity});
}
stack_u8.push(4) catch |err| try stdout.print("stack_u8.push: {}\n", .{err});
// データの取得
for(0..stack_u8.buffer.len) |_| {
    try stdout.print("stack_u8.pop() = {!}\n", .{ stack_u8.pop() });
}
try stdout.print("stack_u8.pop() = {!}\n", .{ stack_u8.pop() });

// 文字列を扱うスタック
var stack_str = try StackAlloc([:0]const u8).init(alloc, 3);
defer stack_str.deinit();
// 文字列の格納
for(&[_][:0]const u8{ "one", "two", "three" }) |s| {
    try stack_str.push(s);
    try stdout.print("stack_str.push(\"{s}\"): stack_str.capacity = {}\n", .{s, stack_str.capacity});
}
stack_str.push("four") catch |err| try stdout.print("stack_str.push: {}\n", .{err});
// 文字列の取得
for(0..stack_str.buffer.len) |_| {
    try stdout.print("stack_str.pop() = {!s}\n", .{ stack_str.pop() });
}
try stdout.print("stack_str.pop() = {!s}\n", .{ stack_str.pop() });
結果
stack_u8.push(1): stack_u8.capacity = 2
stack_u8.push(2): stack_u8.capacity = 1
stack_u8.push(3): stack_u8.capacity = 0
stack_u8.push: error.StackFull
stack_u8.pop() = 3
stack_u8.pop() = 2
stack_u8.pop() = 1
stack_u8.pop() = error.StackEmpty
stack_str.push("one") / stack_str.capacity = 2
stack_str.push("two") / stack_str.capacity = 1
stack_str.push("three") / stack_str.capacity = 0
stack_str.push: error.StackFull
stack_str.pop() = three
stack_str.pop() = two
stack_str.pop() = one
stack_str.pop() = error.StackEmpty

容量をコンパクトにするpacked struct

packed structは可能な限り値が持つメモリ容量をコンパクトにするものです。u4型のフィールドを2つもつ型をstructpacked structのそれぞれで定義し、@sizeOf関数[2]でメモリ容量を比較してみます。

容量をコンパクトにするpacked struct
const RawBits = struct {    // 値の容量は2バイト
    low: u4,
    high: u4,
};

const PackedBits = packed struct {  // 値の容量は1バイト
    low: u4,
    high: u4,
};

try stdout.print("@sizeOf(RawBits) = {}, @sizeOf(PackedBits) = {}\n", .{ @sizeOf(RawBits), @sizeOf(PackedBits) });
結果
@sizeOf(RawBits) = 2, @sizeOf(PackedBits) = 1

structu4型のフィールドが2つでメモリ容量が2バイト必要だとわかります。一方packed structは同じu4型のフィールド2つですがメモリ容量は1バイトに収まっています。これは2つのフィールドでビット長の合計が8ビットになっているためです。このようにpacked structでは可能な限りメモリ容量をコンパクトにできます。実行環境のメモリ容量が限られるときに有効です。

次にそれぞれの型で値を定義し、メモリ上にどう格納されるかをstd.mem.toBytes関数[3]で確認してみます。

struct, packed structのメモリ内での格納
// structの値がどうメモリに格納されるか
const rb = RawBits{ .low = 0xa, .high = 0xb };
try stdout.print("std.mem.toBytes(rb):", .{});
for(std.mem.toBytes(rb)) |v| {
    try stdout.print(" {x:0>2}", .{v});
}
try stdout.print("\n", .{});

// packed structの値がどうメモリに格納されるか
const pb = PackedBits{ .low = 0xa, .high = 0xb };
try stdout.print("std.mem.toBytes(pb):", .{});
for(std.mem.toBytes(pb)) |v| {
    try stdout.print(" {x:0>2}", .{v});
}
try stdout.print("\n", .{});
結果
std.mem.toBytes(rb): 0a 0b
std.mem.toBytes(pb): ba

structのほうは各フィールドの値が別々に格納されていますが、packed structのほうは1バイトの中に2つのフィールドの値が格納されています。

packed structとpacked unionによる値の分割と統合

このpacked structpacked unionを組み合わせると8ビットの値から3ビット、4ビット、1ビットなどと分割して取得したり、これらを統合した8ビットの値を取得したりできます。逆に統合された値を自動的に各フィールドに分割することもできます。制御系(IoT)のように数ビット単位で制御するときに有効です。なお、フィールドごとに分割した値がどのように統合されるかは実行環境によって異なる可能性があります。

packed structとpacked unionの組み合わせで8ビットを分割して制御
const CheckUnionPackedBits = packed union {
    Bits: packed struct(u8) {   // (u8)は構造型全体をまとめたときの型、フィールドのビット長の合計と同じであること
        check: u1,
        low: u4,
        high: u3,
    },
    value: u8,
};
8ビットを複数ビットで分割して制御
// 統合された値(value)を自動的に分割(check, low, high)
var cupb: CheckUnionPackedBits = .{ .value = 0b01010101 };
try stdout.print("cupb.value = 0b{b:0>8}\n", .{cupb.value});
try stdout.print("cupb.Bits.high = 0b{b:0>3}, cupb.Bits.low = 0b{b:0>4}, cupb.Bits.check = 0b{}\n", .{ cupb.Bits.high, cupb.Bits.low, cupb.Bits.check });

// 分割された値(check, low, high)を自動的に統合(value)
cupb.Bits = .{ .high = 0b101, .low = 0b0101, .check = 0 };
try stdout.print("cupb.Bits.high = 0b{b:0>3}, cupb.Bits.low = 0b{b:0>4}, cupb.Bits.check = 0b{}\n", .{ cupb.Bits.high, cupb.Bits.low, cupb.Bits.check });
try stdout.print("cupb.value = 0b{b:0>8}\n", .{cupb.value});
結果
cupb.value = 0b01010101
cupb.Bits.high = 0b010, cupb.Bits.low = 0b1010, cupb.Bits.check = 0b1
cupb.Bits.high = 0b101, cupb.Bits.low = 0b0101, cupb.Bits.check = 0b0
cupb.value = 0b10101010

また、容量はコンパクトにならないものの、RGBによる色設定にも応用できます。

RGBによる色設定
const UC = packed union {
    hex: u24,
    color: packed struct(u24) {
        blue: u8,
        green: u8,
        red: u8,
    },
};

// 統合された値(hex)を自動的に分割(blue, green, red)
var uc = UC{ .hex = 0xabcdef };
try stdout.print("uc.hex = 0x{x}\n", .{uc.hex});
try stdout.print("uc.color.red = 0x{x}, uc.color.blue = 0x{x}, uc.color.green = 0x{x}\n", .{ uc.color.red, uc.color.green, uc.color.blue });

// 分割された値(blue, green, red)を自動的に統合(hex)
uc.color = .{ .red = 0xfe, .green = 0xdc, .blue = 0xba };
try stdout.print("uc.color.red = 0x{x}, uc.color.blue = 0x{x}, uc.color.green = 0x{x}\n", .{ uc.color.red, uc.color.green, uc.color.blue });
try stdout.print("uc.hex = 0x{x}\n", .{uc.hex});
結果
uc.hex = 0xabcdef
uc.color.red = 0xab, uc.color.blue = 0xcd, uc.color.green = 0xef
uc.color.red = 0xfe, uc.color.blue = 0xdc, uc.color.green = 0xba
uc.hex = 0xfedcba

変数に代入しない構造型 (無名あるいは匿名の構造型)

構造型(struct)は変数に代入せず型名の代わりに宣言できます。型の内容がわかりにくくなる可能性がありますが、とりあえずテストしてみるときには良いかもしれません。

変数に代入しない構造型
// 関数の戻り値にstruct
fn appendDepth(plain: anytype, depth: i8)
	struct { width: i8, height: i8, depth: i8 } {
	// plainはwidthとheightというフィールドを持つことが必須
	return .{ .width = plain.width, .height = plain.height, .depth = depth };
}

// 変数の型にstruct
const plain: struct { width: i8, height: i8 } = .{ .width = 10, .height = 20 };
const solid = appendDepth(plain, 30);
try stdout.print("solid = (width: {}, height: {}, depth: {})\n", .{ solid.width, solid.height, solid.depth });
結果
solid = (width: 10, height: 20, depth: 30)

フィールド名を定義せずタプルとしての宣言もできます。

タプルで宣言
fn initMenu(name: [:0]const u8, price: u16) struct { [:0]const u8, u16 } {
	return .{ name, price };
}

const menu = initMenu("コーヒー", 450);
try stdout.print("name = {s}, price = {}\n", .{ menu[0], menu[1] });
結果
name = コーヒー, price = 450

@importとstruct

@importで構造型を含むソースコードを読み込み、定義を利用できます。これにはソースコードに構造型以外を含む場合と、ソースコード全体を1つの構造型の定義とする場合があります。どちらにも共通しているのは、アクセス可能な変数や関数は宣言の先頭にpubをつけることです。

@importで読み込むソースコードに構造型以外を含むときは、@import("ソースファイル名").型名で呼び出します。

ソースコードの読み込みと定義の呼び出し
const stack = @import("stack.zig");
const StackU8 = stack.StackU8;
const StackAlloc = stack.StackAlloc;

var buf: [3]u8 = undefined;
var stacku8 = StackU8.init(&buf);

var stack_str = try StackAlloc([:0]const u8).init(alloc, 3);
defer stack_str.deinit();
stack.zig
const std = @import("std");

pub const StackU8 = struct {
    buffer: []u8,

    pub fn init(buffer: []u8) Self {
        return .{ .buffer = buffer };
    }

    // ..... (snip) .....
};

pub fn StackAlloc(comptime T: type) type {
    return struct {
        allocator: std.mem.Allocator,
        buffer: []T,
        capacity: usize,

        const Self = @This();

        pub fn init(allocator: std.mem.Allocator, capacity: usize) !Self {
            return .{
                .allocator = allocator,
                .buffer = try allocator.alloc(T, capacity),
                .capacity = capacity
            };
        }
        pub fn deinit(self: *Self) void {
            self.allocator.free(self.buffer);
        }

        // ..... (snip) .....
    };
}

読み込むソースコードが構造型の定義からstruct {}をなくしたものであるとき、全体を1つの構造型の定義として扱います。

読み込むソースコード全体が1つの構造型のみの場合
const PointS = @import("point_struct.zig");
var pts = PointS.init(3, 5);
point_struct.zig
x: i8,
y: i8,

const Self = @This();

pub fn init(x: i8, y: i8) Self {
    return .{ .x = x, .y = y };
}

構造型を扱う標準ライブラリの関数

標準ライブラリには構造型を扱う関数が定義されています。これらの特徴はフィールド名や変数名などを文字列で指定できることです。

@hasField(型名, "フィールド名")

構造型が指定したフィールドを持っているかを確認しbool型で返します。

@hasField(PointS, "x") == true
@hasField(PointS, "init") == false

@hasDecl(型名, "変数名または関数名")

構造型が指定した変数や関数を持っているかを確認しbool型で返します。

@hasDecl(PointS, "y") == false
@hasDecl(PointS, "init") == true

@field(変数名, "フィールド名")

指定したフィールドの値を参照または更新します。

var pts = PointS.init(3, 5);
@field(pts, "x") = 8;
_ = @field(pts, "x");  // 8

@fieldParentPtr("フィールド名", フィールドのポインタ)

フィールド名とフィールドのポインタから構造型のポインタを取得します。

const pt: *PointS = @fieldParentPtr("x", &pts.x);
try stdout.print("pt.x = {}, pt.y = {}\n", .{ pt.x, pt.y });  // pt.x = 8, pt.y = 5

まとめ

  • 構造型(Struct Type)はいくつかのフィールドがすべて値を持つ型である
  • Zigにおいて型は値なので変数に代入したり、関数の引数や戻り値として設定できる
  • 構造型はstruct { フィールド名: 型名, ... }で定義する
  • 構造型の値は structの型名{ .フィールド名 = 値, ...}で定義する
  • 各フィールドの値は変数.フィールド名で表し、構造型の値がvarで代入されていればフィールドの値を変更できる
  • フィールドのデフォルト値はフィールド名: 型名 = 値で設定でき、構造型の値を設定するときに省略されたフィールドにはこの値が設定される
  • 構造型にはメソッド(method)とメンバ関数(member function)を定義できる
    • メソッドは同じ型であればどんな値にも適用される
    • メンバ関数はフィールドの値を変更しないとき第1引数が@This()型、変更するときは*@This()型とする
  • 構造型に定義する変数の値は同じ構造型のすべての値が影響を受ける
    • この変数の値は外部から変更される可能性がある(ただし@importでソースコードを読み込む場合は例外がある)
  • 構造体のinit関数は値の初期化、deinit関数は実行環境を値の使用前と同じ状態に戻すように定義する
  • 構造型のポインタでフィールドの値を表すときはポインタ変数名.フィールド名もしくはポインタ変数名.*.フィールド名とする
  • フィールドの型を任意で指定できるようにするには構造型を返す関数の引数で型を指定する
  • packed structは必要なメモリ容量をコンパクトにできる
  • packed structpacked unionを組み合わせると複数の値を1つに統合したり、統合された値を複数に分割したりできる
  • 構造型を変数に代入せず、型名の代わりに宣言できるが、型の内容がわかりにくくなる可能性もある
  • 構造型の定義からフィールド名をなくすとタプルとして宣言できる
  • @importでソースコードを読み込むとき、それに構造型以外が含まれるときは@import("ソースファイル名").型名で呼び出し、それが構造型の定義からstruct {}を省略したものだけの場合はファイル全体を構造型の定義として扱う
  • @importで読み込まれたソースコードは先頭にpubが指定されているものだけにアクセスできる
  • 構造型を扱う標準ライブラリの関数には@hasField[4]@hasDecl[5]@field[6]@fieldParentPtr[7]がある

< アロケータの使い方
キャスト(明示的な型変換) >
ざっくりZig 一覧


脚注
  1. @TypeOf ↩︎

  2. @sizeOf ↩︎

  3. std.mem.toBytes ↩︎

  4. @hasField ↩︎

  5. @hasDecl ↩︎

  6. @field ↩︎

  7. @fieldParentPtr ↩︎

Discussion

ktz_aliasktz_alias

deinitについて

インスタンス自体をヒープアロケーションする場合、deinitで自殺するコードをよく書きます。

pub fn init(allocator: Allocator) !Self {
    const self = try allocator.create(Self);
    self.* = .{
        .allocator = allocator,
        // (snip)
    };
    return self;
}
pub fn deinit(self: *Self) void {
    // (snip)
    self.allocator.destroy(self);
    self.* = undefined;
}

インスタンスの自殺なんて書いたのC++以来やわ〜

@importとstructのファイルコンテナの場合について

zigのドキュメントに言語の命名ルールとしてファイルコンテナの方の場合は、ファイル名をTitleCaseにとの記述がなされています。
そうしなければならないというわけではないでしょうが、したがっておいた方が無難かと思われます。
(point_struct.zigではなくPoint.zigにする感じ)

https://ziglang.org/documentation/master/#Names

構造型を扱う標準ライブラリの関数について

std.metaの各種関数も結構便利。

  • std.meta.fields - structに限らず、enum, unionのフィールド情報を列挙
  • std.meta.hasFn - メソッドが定義されているかのチェック

この辺りはよく使いますね。