構造体 (struct
) はデータ構造の1つで、複数の異なる値を1つにまとめることができます。Zenの構造体では、関連する関数を構造体に定義することができます。
Zenの構造体は、JavaやC++などのクラスに近いものですが、継承やコンストラクタ/デストラクタがない、という点で明確に異なります。Zenでのプログラミングは、構造体を中心としたオブジェクトベースの手続き型であり、オブジェクト指向でないことに注意して下さい。
クラスの継承は、そのクラスが備えている機能の一部が見えにくくなってしまいます。Zenでは他の構造体が持つ機能が必要であれば、フィールドとしてコンポジションします。Zenの重要な設計思想は、全て明確に書く、ということなのです。
まず、構造体を定義する方法を解説します。10個までi32
の値が格納できるスタックFixedStack
を作りながら、見ていきましょう。
構造体の定義は、const 型名 = struct { ... };
で行うことができます。struct { ... };
によって新しい構造体を宣言し、const 型名 =
で型名を定義しています。
examples/ch03-user-defined-types/src/struct_define.zen:4:6
const FixedStack = struct {
// ここに構造体の中身を宣言する
};
struct { ... };
の...
部分には、変数/定数の定義、フィールドの宣言、メソッドの定義、が可能です。
定義した構造体の型を使って、インスタンスを作成します。構造体インスタンスを初期化する構文は、const / var インスタンス名 = 型名 { フィールド初期化, ... };
です。フィールドが存在しない構造体もインスタンスを生成することができます。
examples/ch03-user-defined-types/src/struct_define.zen:8:10
test "instantiate struct" {
const stack = FixedStack{};
}
構造体が持つ値をフィールドと呼びます。フィールドの宣言は、フィールド名: 型名,
です。型名を省略できないことと、最後に,
が必要なことに注意して下さい。次のbuf
とtop
がフィールドに該当します。
examples/ch03-user-defined-types/src/struct_field.zen:4:9
const FixedStack = struct {
// スタックのデータ保存に使用するメモリ領域
buf: [10]i32,
// スタックの先頭を指すインデックス
top: usize,
};
フィールドを持つ構造体は、初期化時に全てのフィールドを初期化する必要があります。構造体のフィールドは、.フィールド名 = 初期値,
で初期化できます。ここでも,
が必要です。
ノート: 構文上、構造体の最後に書かれたフィールド (上記コードの
top
) には、末尾の,
があってもなくてもかまいません。
examples/ch03-user-defined-types/src/struct_field.zen:11:15
test "field of a struct" {
const stack = FixedStack{
.buf = [_]i32{0} ** 10,
.top = 0,
};
}
上記の例では、stack
フィールドの配列要素を全てと、top
フィールドを0
で初期化しています。
フィールドへのアクセスは、メンバアクセス (.
) 演算子を使います。インスタンス名.フィールド名
で値へのアクセスが可能です。
examples/ch03-user-defined-types/src/struct_field.zen:17:17
ok(stack.top == 0);
フィールドに再代入する (値を更新する) 場合は、var
で構造体インスタンスを生成します。
examples/ch03-user-defined-types/src/struct_field.zen:20:27
test "write to a field" {
var stack = FixedStack{
.buf = [_]i32{0} ** 10,
.top = 0,
};
stack.top = 1;
ok(stack.top == 1);
}
const
で構造体インスタンスを生成した場合、初期値から値を更新することはできません。
構造体の中にvar
による変数定義とconst
による定数定義が可能です。これは構造体の名前空間に区切られた変数/定数として利用できます。
ノート:
var
で定義された変数は、構造体の名前空間で区切られたグローバル変数です。構造体インスタンスごとに値を持つわけではないことに注意して下さい。
定義は通常のvar
やconst
を使った定数の定義と同じです。
examples/ch03-user-defined-types/src/struct_const.zen:4:13
const FixedStack = struct {
// スタックのサイズ
const SIZE: usize = 10;
// スタックのデータ保存に使用するメモリ領域
// 構造体内では`SIZE`で値を利用可能
buf: [SIZE]i32,
// スタックの先頭を指すインデックス
top: usize,
};
このコードではusize
で型名を明記していますが、コンパイラによる型推論が可能な場合は、型名を省略可能です。構造体内では、定義した定数をそのまま利用できます (構造体名.定数
でも利用可能です) 。
構造体の外部から変数/定数を利用する場合、メンバアクセス (.
) 演算子を使い構造体名.定数
とします。
examples/ch03-user-defined-types/src/struct_const.zen:15:25
test "const in a struct" {
// 構造体の外からは、`型名.定数名`でアクセス可能
ok(FixedStack.SIZE == 10);
const stack = FixedStack{
.buf = [_]i32{0} ** FixedStack.SIZE,
.top = 0,
};
// Compile error: no member named 'SIZE' in struct
// ok(stack.SIZE == 10);
}
構造体内部の変数/定数は、構造体の名前空間に区切られた変数/定数として扱われるため、インスタンス名.定数
ではアクセスできません。
構造体の中に関数を定義できます。構造体内部に定義した関数のうち、ある条件を満たした関数をメソッドと呼びます。
まずは通常の関数定義です。通常通り関数を定義すると、構造体の名前空間内に定義された関数として扱われます。
examples/ch03-user-defined-types/src/struct_functions.zen:4:46
const FixedStack = struct {
// ...
// スタックの最大サイズを返す関数
fn capacity() usize {
return SIZE;
}
};
この関数は、構造体名.関数名
で呼び出します。
examples/ch03-user-defined-types/src/struct_functions.zen:48:50
test "namespaced function" {
ok(FixedStack.capacity() == 10);
}
続いて、メソッドです。メソッドは第一引数に自身の構造体インスタンス (もしくはそのポインタ) を受け取る関数です。
まず構造体インスタンスを受け取るメソッドのコードを示します。
examples/ch03-user-defined-types/src/struct_functions.zen:19:21
fn getTop(self: FixedStack) usize {
return self.top;
}
第一引数self: FixedStack
で自身の構造体インスタンスを受け取っています。引数名は慣習的にself
とすることが多いです。self.top
でインスタンスのtop
フィールドにアクセスします。このtop
の値はインスタンスごとに異なります。
メソッドは、インスタンス名.メソッド名( 第二引数以降の引数, ... )
で呼び出すことができます。メソッドの引数は、第二引数以降を渡します。第一引数の構造体インスタンスは、暗黙的に渡されます。
examples/ch03-user-defined-types/src/struct_functions.zen:52:65
test "get top of a stack" {
const stack1 = FixedStack{
.buf = [_]i32{0} ** FixedStack.SIZE,
.top = 0,
};
ok(stack1.getTop() == 0);
const stack2 = FixedStack{
.buf = [_]i32{0} ** FixedStack.SIZE,
.top = 2,
};
ok(stack2.getTop() == 2);
ok(FixedStack.getTop(stack2) == 2);
}
ノート:
インスタンス名.メソッド名( 第二引数以降の引数, ... )
は、構造体名.メソッド名( 第一引数, 第二引数, ... )
のシンタックスシュガーです。上のコードで、stack1.getTop()
とFixedStack.getTop(stack1)
は同等です。
構造体インスタンスを受け取る場合、その引数として受け取ったインスタンスはイミュータブルです。
fn getTop(self: FixedStack) usize {
self.top = 1;
return self.top;
}
上のコードのようにフィールドを更新しようとすると、コンパイルエラーになります。
error[E02030]: cannot assign to constant variable
self.top = 1;
^
ミュータブルな構造体インスタンスを受け取る場合には、構造体インスタンスのミュータブルなポインタとして引数を指定します。
examples/ch03-user-defined-types/src/struct_functions.zen:23:29
// スタックの先頭に新しいデータを積むメソッド
fn push(self: *mut FixedStack, data: i32) void {
if (self.top < SIZE) {
self.buf[self.top] = data;
self.top += 1;
}
}
第一引数として、self: *mut FixedStack
を受け取っています。この場合、self
の参照先はミュータブルです。そのため、self.buf
やself.top
を更新することができます。
examples/ch03-user-defined-types/src/struct_functions.zen:67:76
test "update instance field" {
var stack = FixedStack{
.buf = [_]i32{0} ** FixedStack.SIZE,
.top = 0,
};
stack.push(5);
// インスタンスのフィールドが更新されている
ok(stack.buf[0] == 5);
ok(stack.top == 1);
}
ここで、stack.push(5)
は、FixedStack.push(&stack, 5)
のシンタックスシュガーです。push
メソッドでは、インスタンス変数stack
を参照するポインタを受け取ることになります。
上のコードでは、stack
をvar
で定義しました。const
で定義すると、イミュータブルオブジェクトへのポインタになるため、push
の呼び出しはコンパイルエラーになります。
const stack = FixedStack {
.buf = [_]i32{0} ** FixedStack.SIZE,
.top = 0,
};
stack.push(5);
error: expected '*mut src.struct_functions.FixedStack', found '*src.struct_functions.FixedStack'
stack.push(5);
^
*mut FixedStack
を要求していますが、*FixedStack
が渡されているので、コンパイルエラーになります。これは、*FixedStack
の方が制限が強い型であるためです。逆の場合 (*FixedStack
を要求するところに、*mut FixedStack
を渡す場合) は、暗黙の型変換によりコンパイルが可能です。
メソッド内から別のメソッドを呼び出すこともできます。
examples/ch03-user-defined-types/src/struct_functions.zen:31:45
// スタックの先頭からデータを取り出すメソッド
fn pop(self: *mut FixedStack) ?i32 {
// `self`を使って別メソッドを呼び出す
if (self.isEmpty()) {
return null;
}
self.top -= 1;
return self.buf[self.top];
}
// スタックが空なら`true`を返すメソッド
fn isEmpty(self: *FixedStack) bool {
return self.top == 0;
}
構造体の中には、フィールドの宣言、変数/定数の定義、関数の定義を記述することができますが、その順序には1つだけ制限があります。
フィールドとフィールドの宣言の間に、変数/定数の定義および関数の定義を記述することはできません。 次の2つの例は、どちらもコンパイルエラーになります。
const S = struct {
field_a: usize,
const b: usize = 0;
field_b: usize,
};
error[E03054]: declarations are not allowed between container fields
const b: usize = 0;
~
const S = struct {
field_a: usize,
fn b() void { return ;}
field_b: usize,
};
error[E03054]: declarations are not allowed between container fields
fn b() void { return ;}
~
これまで、構造体の初期化は明示的にフィールドに初期値を与えていました。
const stack1 = FixedStack{
.buf = [_]i32{0} ** FixedStack.SIZE,
.top = 0,
};
このままでは不便なので、初期化用と後処理用の関数を用意します。慣習的に初期化にはinit
関数を、後処理にはdeinit
関数を用意します。
init
関数では新しいインスタンスを生成します。
examples/ch03-user-defined-types/src/struct_init.zen:14:20
// ゼロクリアした`FixedStack`のインスタンスを返す
fn init() FixedStack {
return FixedStack{
.buf = [_]i32{0} ** SIZE,
.top = 0,
};
}
初期化処理によっては、初期値を引数渡しする実装も考えられます。例えば、上のinit
を次のように実装しても良いでしょう。これはデフォルトの初期化をどのようにするか、という設計の問題になります。
fn init(buf: [SIZE]i32, top: usize) FixedStack {
return FixedStack {
.buf = buf,
.top = top,
};
}
deinit
関数では、構造体インスタンスの後処理を行います。典型的には、動的に確保したリソースの解放が挙げられます。今回は、そのようなリソースはないため、フィールドをゼロクリアする処理を実装します。
examples/ch03-user-defined-types/src/struct_init.zen:22:28
// インスタンスをゼロクリアする
fn deinit(self: *mut FixedStack) void {
for (self.buf) |*value| {
value.* = 0;
}
self.top = 0;
}
ノート:
self.buf = [_]i32{0} ** SIZE
としてもbuf
をゼロクリアできます。
init
関数とdeinit
関数の利用例は次の通りです。
examples/ch03-user-defined-types/src/struct_init.zen:72:87
test "init deinit" {
// 初期化
var stack = FixedStack.init();
equalSlices(i32, &[_]i32{0} ** FixedStack.SIZE, &stack.buf);
ok(stack.top == 0);
// インスタンスを使用
stack.push(5);
ok(stack.buf[0] == 5);
ok(stack.top == 1);
// 後処理
stack.deinit();
equalSlices(i32, &[_]i32{0} ** FixedStack.SIZE, &stack.buf);
ok(stack.top == 0);
}
構造体のフィールドを宣言する際に、デフォルト値を設定することができます。デフォルト値が設定されたフィールドは、構造体インスタンス生成時にフィールドの初期化が不要です。
デフォルト値は、フィールドの宣言に続いて、= 初期値
で設定します。
examples/ch03-user-defined-types/src/struct_default.zen:5:67
const FixedStack = struct {
// ...
// スタックのデータ保存に使用するメモリ領域
buf: [SIZE]i32 = [_]i32{0} ** SIZE,
// スタックの先頭を指すインデックス
top: usize = 0,
// ...
};
デフォルト値を設定した後は、構造体インスタンスを次のように作成できます。
examples/ch03-user-defined-types/src/struct_default.zen:69:73
test "default value" {
var stack = FixedStack{};
equalSlices(i32, &[_]i32{0} ** FixedStack.SIZE, &stack.buf);
ok(stack.top == 0);
}
一部、もしくは全てのフィールドのデフォルト値を上書きして構造体インスタンを生成することが可能です。次のコードでは、top
フィールドのみデフォルト値を上書きしています。
examples/ch03-user-defined-types/src/struct_default.zen:75:81
test "override default value" {
var stack = FixedStack{
.top = 5,
};
equalSlices(i32, &[_]i32{0} ** FixedStack.SIZE, &stack.buf);
ok(stack.top == 5);
}
構造体型、関数、メソッド、変数、定数の可視性をコントロールすることができます。ここまで実装したFixedStack
をfixed_stack.zen
ファイルに保存します。
const FixedStack = struct {
// ...
};
このFixedStack
は、現時点では、fixed_stack.zen
ファイル (名前空間) の中でのみ利用可能です。これは、FixedStack
構造体型の可視性がプライベートになっているためです。
新しくuse_fixed_stack.zen
というファイルをfixed_stack.zen
と同じディレクトリ内に次の内容で作成し、コンパイルすると、コンパイルエラーになります。
examples/ch03-user-defined-types/src/use_fixed_stack.zen:3:7
const FixedStack = @import("fixed_stack.zen").FixedStack;
test "use FixedStack" {
var stack = FixedStack.init();
}
error: 'FixedStack' is private
const FixedStack = @import("fixed_stack.zen").FixedStack;
^
外部の名前空間に公開する関数、変数、定数、型には、pub
修飾子をつけて、可視性をパブリックにします。
examples/ch03-user-defined-types/src/fixed_stack.zen
// `FixedStack`型を公開する
pub const FixedStack = struct {
// ...
// `init`関数を公開する
pub fn init() FixedStack {
// ...
};
pub
をつけることにより、外部からFixedStack
やinit
関数を使用するコードがコンパイルできるようになります。
examples/ch03-user-defined-types/src/use_fixed_stack.zen
test "use FixedStack" {
var stack = FixedStack.init();
}
ノート: フィールドの可視性は、デフォルトでは全てプライベートとなり、個別に
pub
を指定したフィールドのみパブリックとなります。ただし、構造体に関数が定義されていない場合、および構造体がextern
指定されている場合は、全フィールドがパブリックとなります。
構造体インスタンスに対して、ポインタによる参照が可能です。ポインタを得るためには、アドレス (&
) 演算子を使用します。
examples/ch03-user-defined-types/src/struct_pointer.zen:3:7
const FixedStack = @import("fixed_stack.zen").FixedStack;
test "create pointer to a struct" {
var stack = FixedStack.init();
const pointer = &stack;
構造体インスタンスへのポインタから、フィールドやメソッドを使う場合、デリファレンスを省略することができます。
examples/ch03-user-defined-types/src/struct_pointer.zen:9:11
// 下の2つは同等
ok(pointer.getTop() == 0);
ok(pointer.*.getTop() == 0);
構造体インスタンスへのポインタに対してメンバアクセス (.
) 演算子が使用されていると、Zenコンパイラが自動的にデリファレンスします。
関数に対して、構造体インスタンスを値渡しする次のコードを書いたとします。
const SmallStruct = struct {
value: usize = 0,
};
fn receiveStruct(given: SmallStruct) void {
// 何らかの処理
}
test "pass small object as an argument" {
const obj = SmallStruct {};
receiveStruct(obj);
}
このとき、引数としてやり取りされる構造体インスタンスに対して、2つのことが言えます。
上記の例では、receiveStruct
関数内でgiven
はイミュータブルです。また receiveStruct
の given
は、多くの場合、呼び出し側のobj
を参照する機械語に変換されます。Zenコンパイラが引数の値をコピーした方が効率が良いか、参照で渡した方が効率が良いか、を判定して、コードを最適化します。
呼び出し側と呼び出された側とで、同じ構造体インスタンスが参照されていることが、下のコードからわかります。
examples/ch03-user-defined-types/src/struct_as_args.zen:4:15
const SmallStruct = struct {
value: usize = 0,
};
fn receiveStruct(given: SmallStruct, addr: usize) void {
ok(@ptrToInt(&given) == addr);
}
test "pass small object as an argument" {
const obj = SmallStruct{};
receiveStruct(obj, @ptrToInt(&obj));
}
関数内で、引数として受け取った構造体インスタンスを変更する必要がない場合、コピーコストを気にすることなく、値渡しをする形でプログラムを記述することができます。
もちろん明示的にポインタを渡すことも可能です。
fn receiveImmutablePointer(given: *SmallStruct) void {
// ...
}
引数として渡す構造体インスタンスへの変更を伴う場合は mut
をつけたミュータブルなポインタにする必要があります。
fn receiveMutablePointer(given: *mut SmallStruct) void {
// ...
}
構造体をフィールドとして持つ構造体を宣言できます。デフォルト値の設定など、通常のプリミティブ型をフィールドに持つ場合と同じ操作が可能です。
examples/ch03-user-defined-types/src/struct_nested.zen:4:21
const Inner = struct {
value: usize = 0,
};
const Outer = struct {
value: Inner = Inner{},
};
test "nested struct" {
var default = Outer{};
var override = Outer{
.value = Inner{
.value = 4,
},
};
ok(override.value.value == 4);
}
構造体の宣言内で、組込み関数@This
を使用すると、自身の型名を得ることができます。このことを利用して、Self
という型エイリアスを作成し、宣言内に具体的な型名を書く回数を減らすことが可能です。
examples/ch03-user-defined-types/src/struct_namelss.zen:4:21
const MyStruct = struct {
// 自身の型名を得て、`Self`というエイリアスを作る
const Self = @This();
fn init() Self {
return Self{};
}
fn doSomething(self: Self) void {
ok(Self == MyStruct);
}
};
test "@This" {
var x = MyStruct.init();
ok(@TypeOf(x) == MyStruct);
x.doSomething();
}
完全に無名な構造体を定義し、リテラルからインスタンスを作成することができます。
無名構造体リテラルは .{ ... }
の文法で利用します。
...
の箇所にフィールドとなるリテラルを記述します。
例えば、u32
のx
, bool
のy
, []u8
のz
, をフィールドに持つ無名構造体リテラルは次のように書けます。
.{ .x = @to(u32, 42), .y = true, .z = "hi" }
無名構造体リテラル内に関数や const
/ var
を記述することはできません。
.{
.x = @to(u32, 42),
// 次の2行はいずれもコンパイルエラーです
fn y() void { return; }
const z: usize = 0;
}
フィールドへのアクセスは通常の構造体と同様、インスタンス名.フィールド名
です。
examples/ch03-user-defined-types/src/anonymous_struct.zen:5:10
test "anonymous struct" {
const anon = .{ .x = @to(u32, 42), .y = true, .z = "hi" };
ok(anon.x == 42);
ok(anon.y);
equalStrings("hi", anon.z);
}
関数引数として無名構造体を使うこともできます。
その場合、引数の型はanytype
を指定します。
examples/ch03-user-defined-types/src/anonymous_struct.zen:12:20
fn receive(args: anytype) void {
ok(args.x == 42);
ok(args.y);
equalStrings("hi", args.z);
}
test "anonymous struct to argument" {
receive(.{ .x = @to(u32, 42), .y = true, .z = "hi" });
}
無名構造体リテラルは、無名フィールドを持つことができます。 次のようにフィールドを指定せず、フィールドへのリテラルを記述します。
.{ @to(u32, 42), true, "hi" }
無名フィールドは、[]
によるインデックスアクセスが可能です。
examples/ch03-user-defined-types/src/anonymous_struct.zen:22:27
test "anonymous struct can have anonymous fields" {
const anon = .{ @to(u32, 42), true, "hi" };
ok(anon[0] == 42);
ok(anon[1]);
equalStrings("hi", anon[2]);
}
無名構造体リテラルから構造体へ、暗黙の型変換が可能です。 無名構造体リテラルのフィールド名とフィールド型とが一致していることが条件です。
examples/ch03-user-defined-types/src/anon_struct_cast.zen:5:26
const Args = struct {
x: u32,
y: bool,
z: []u8,
};
fn receive(args: Args) void {
ok(args.x == 42);
ok(args.y);
equalStrings("hi", args.z);
}
test "casting anonymous struct to struct" {
// 無名構造体を Args に型変換します
const args: Args = .{ .x = @to(u32, 42), .y = true, .z = "hi" };
ok(args.x == 42);
ok(args.y);
equalStrings("hi", args.z);
// 無名構造体を receive の引数型 Args に型変換します
receive(.{ .x = @to(u32, 42), .y = true, .z = "hi" });
}
少し高度な使い方になりますが、ジェネリック関数と合わせて使用すると複雑な型を省略することができます。
examples/ch03-user-defined-types/src/generic_anon_struct_cast.zen:5:21
fn VeryComplexType(comptime T: type) type {
return struct {
value: T,
};
}
fn compare(a: anytype, b: @TypeOf(a)) bool {
return a.value == b.value;
}
test "generic anonymous struct cast" {
const unsigned = VeryComplexType(u32){ .value = 42 };
ok(compare(unsigned, .{ .value = 42 }));
const float = VeryComplexType(f64){ .value = 2.0 };
ok(compare(float, .{ .value = 2.0 }));
}
☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 connectFree Corporation. All rights reserved. | 特定商取引法に基づく表示
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.