ざっくりZig - structは構造型、型はtype、typeは値
struct {...}
は構造型(Struct Type)といい、基本的には名前と値の組であるフィールド(Field)をいくつか持ち、そのすべてが値を持ちます。デフォルトはそれぞれのフィールドを別々のメモリ領域で持ちますが、同じメモリ領域に複数のフィールドを持つこともできます。
引数の型を得られる@TypeOf
関数[1]で@TypeOf(struct{})
の結果はtype
です。これは@TypeOf(u8)
などと同じです。つまりstruct {...}
はu8
などと同様に型であることがわかります。Zigにおいて型はtype型の値なので、変数に代入したり、関数の引数にしたり、関数の最後にreturn
で返したりできます。
構造型の定義
構造型はstruct { フィールド名: 型名, ... }
で定義します。x
, y
という2つのフィールドを持つPoint
型を定義するとこうなります。
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
型の値となります。
const p1 = Point{ .x = 3, .y = 5 }; // すべてのフィールドが値を持つ
各フィールドの値は変数.フィールド名
で表します。構造型の値がvar
で代入されていればフィールドの値を変更できます。
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
フィールドにデフォルト値を設定するときは型名の後に= デフォルト値
を追加します。構造型の値を定義する際にフィールドが省略されるときは、その値を自動的に設定します。
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
の戻り値の型@This
はPoint
型自身を表しています。
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型の値)
となります。
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 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
で定義してみます。deinit
はdefer
によって処理の最後に実行するようにします。
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つもつ型をstruct
とpacked struct
のそれぞれで定義し、@sizeOf
関数[2]でメモリ容量を比較してみます。
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
struct
はu4
型のフィールドが2つでメモリ容量が2バイト必要だとわかります。一方packed struct
は同じu4
型のフィールド2つですがメモリ容量は1バイトに収まっています。これは2つのフィールドでビット長の合計が8ビットになっているためです。このようにpacked struct
では可能な限りメモリ容量をコンパクトにできます。実行環境のメモリ容量が限られるときに有効です。
次にそれぞれの型で値を定義し、メモリ上にどう格納されるかをstd.mem.toBytes
関数[3]で確認してみます。
// 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 struct
にpacked union
を組み合わせると8ビットの値から3ビット、4ビット、1ビットなどと分割して取得したり、これらを統合した8ビットの値を取得したりできます。逆に統合された値を自動的に各フィールドに分割することもできます。制御系(IoT)のように数ビット単位で制御するときに有効です。なお、フィールドごとに分割した値がどのように統合されるかは実行環境によって異なる可能性があります。
const CheckUnionPackedBits = packed union {
Bits: packed struct(u8) { // (u8)は構造型全体をまとめたときの型、フィールドのビット長の合計と同じであること
check: u1,
low: u4,
high: u3,
},
value: u8,
};
// 統合された値(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による色設定にも応用できます。
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();
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つの構造型の定義として扱います。
const PointS = @import("point_struct.zig");
var pts = PointS.init(3, 5);
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 struct
とpacked union
を組み合わせると複数の値を1つに統合したり、統合された値を複数に分割したりできる - 構造型を変数に代入せず、型名の代わりに宣言できるが、型の内容がわかりにくくなる可能性もある
- 構造型の定義からフィールド名をなくすとタプルとして宣言できる
-
@import
でソースコードを読み込むとき、それに構造型以外が含まれるときは@import("ソースファイル名").型名
で呼び出し、それが構造型の定義からstruct {
と}
を省略したものだけの場合はファイル全体を構造型の定義として扱う -
@import
で読み込まれたソースコードは先頭にpub
が指定されているものだけにアクセスできる - 構造型を扱う標準ライブラリの関数には
@hasField
[4]、@hasDecl
[5]、@field
[6]、@fieldParentPtr
[7]がある
< アロケータの使い方
キャスト(明示的な型変換) >
ざっくりZig 一覧
Discussion
deinitについて
インスタンス自体をヒープアロケーションする場合、deinitで自殺するコードをよく書きます。
インスタンスの自殺なんて書いたのC++以来やわ〜
@importとstructのファイルコンテナの場合について
zigのドキュメントに言語の命名ルールとしてファイルコンテナの方の場合は、ファイル名を
TitleCase
にとの記述がなされています。そうしなければならないというわけではないでしょうが、したがっておいた方が無難かと思われます。
(point_struct.zigではなくPoint.zigにする感じ)
構造型を扱う標準ライブラリの関数について
std.meta
の各種関数も結構便利。この辺りはよく使いますね。