オフィスアワーがそろそろ始まるよ!()

構造体

構造体 (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 {};
}

フィールド

構造体が持つ値をフィールドと呼びます。フィールドの宣言は、フィールド名: 型名,です。型名を省略できないことと、最後に,が必要なことに注意して下さい。次のbuftopがフィールドに該当します。

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で定義された変数は、構造体の名前空間で区切られたグローバル変数です。構造体インスタンスごとに値を持つわけではないことに注意して下さい。

定義は通常のvarconstを使った定数の定義と同じです。

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: *FixedStack, data: i32) void {
        if (self.top < SIZE) {
            self.buf[self.top] = data;
            self.top += 1;
        }
    }

第一引数として、self: *FixedStackを受け取っています。この場合、selfの参照先はミュータブルです。そのため、self.bufself.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を参照するポインタを受け取ることになります。

上のコードでは、stackvarで定義しました。constで定義すると、イミュータブルオブジェクトへのポインタになるため、pushの呼び出しはコンパイルエラーになります。

    const stack = FixedStack {
        .buf = [_]i32{0} ** FixedStack.SIZE,
        .top = 0,
    };
    stack.push(5);
error: expected '*src.struct_functions.FixedStack' type, found '*const src.struct_functions.FixedStack'
    stack.push(5);
    ^

*FixedStackを要求していますが、*const FixedStackが渡されているので、コンパイルエラーになります。これは、*const FixedStackの方が制限が強い型であるためです。逆の場合 (*const FixedStackを要求するところに、*FixedStackを渡す場合) は、暗黙の型変換によりコンパイルが可能です。

メソッド内から別のメソッドを呼び出すこともできます。

examples/ch03-user-defined-types/src/struct_functions.zen:31:45

    // スタックの先頭からデータを取り出すメソッド
    fn pop(self: *FixedStack) ?i32 {
        // `self`を使って別メソッドを呼び出す
        if (self.isEmpty()) {
            return null;
        }

        self.top -= 1;
        return self.buf[self.top];
    }

    // スタックが空なら`true`を返すメソッド
    fn isEmpty(self: *const FixedStack) bool {
        return self.top == 0;
    }

init関数 / deinit関数

これまで、構造体の初期化は明示的にフィールドに初期値を与えていました。

    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: *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);
}

可視性

構造体型、関数、メソッド、変数、定数の可視性をコントロールすることができます。ここまで実装したFixedStackfixed_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をつけることにより、外部からFixedStackinit関数を使用するコードがコンパイルできるようになります。

examples/ch03-user-defined-types/src/use_fixed_stack.zen

test "use FixedStack" {
    var stack = FixedStack.init();
}

ノート: フィールドの可視性はすべてpubになります。

構造体インスタンスへのポインタ

構造体インスタンスに対して、ポインタによる参照が可能です。ポインタを得るためには、アドレス (&) 演算子を使用します。

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つのことが言えます。

  1. 呼び出された関数側では、値はイミュータブルです
  2. 構造体インスタンスは多くの場合、コピーされません

上記の例では、receiveStruct関数内でgivenイミュータブルです。また receiveStructgivenは、多くの場合、呼び出し側の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 receiveMutablePointer(given: *SmallStruct) void {
    // ...
}

fn receiveImmutablePointer(given: *const 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()

構造体の宣言内で、組込み関数@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();
}

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 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.