コードブロック

Basis
最終更新:

どの言語でも文をひとまとめにする手段はあるが、Zigはさらに一歩進んでいる:文のグループは式と同様に値を生み出すことができる。ブロックの仕組みを理解してうまく活用すれば、コードはよりコンパクトで明快になり、不要なミュータブル変数を減らせる。

コードブロックとは

コードブロック(単にブロックとも呼ぶ)は、波括弧 { } で囲まれた文の列だ。関数の本体はすべてブロックであり、文が書ける場所ならどこでも独立したブロックを書ける:

pub fn main() void {
    // これは関数の本体 — ブロック。

    {
        // これはネストされたブロック — これもブロック。
        const x: i32 = 10;
        _ = x;
    }
}

ブロックは単なるグループ化の手段ではない。スコープ(scope)、すなわちある変数の集合が見えて生きている領域を定義する。スコープについては Simple Variable チェックポイントで簡単に紹介したが、このチェックポイントではその規則を正確に説明する。

スコープと変数の生存期間

ブロック内で宣言された変数は、そのブロックのスコープに属する。宣言に到達した時点で生まれ、ブロックが終了すると破棄される — メモリが解放される。どの変数も、それを囲むブロックよりも長く生きることはできない。

pub fn main() void {
    const a: i32 = 1; // 'a' は main 全体でスコープに入る

    {
        const b: i32 = 2; // 'b' はこのブロック内だけでスコープに入る
        _ = a;            // 外側の変数はここからも見える
        _ = b;
    }                     // 'b' はここで破棄される

    _ = a;                // 'a' はまだ生きている
    // _ = b;             // コンパイルエラー: 'b' はスコープ外
}

可視性に関して、二つの規則がある:

  • 内側のスコープは外側のスコープの変数を読んで使える
  • 外側のスコープは内側のスコープで宣言された変数を見ることができない

この設計は単なる整理術ではない。意図的なカプセル化の一形態だ:局所的な問題を解くために変数を導入しても、周囲の名前空間を汚染せずに済む。ブロックの外部が誤ってその変数に依存することを、コンパイラが保証してくれる。

シャドーイング(shadowing)

Zigではシャドーイングが許可されている:内側のスコープで外側のスコープと同じ名前の変数を宣言することだ。内側の宣言は、その内側スコープの中でのみ外側の宣言を一時的に隠す。

pub fn main() void {
    const x: i32 = 10;

    {
        const x: i32 = 99; // 外側の 'x' をシャドーイングする
        _ = x;             // 99 を指す(10 ではない)
    }

    _ = x; // 再び外側の 'x' を指す: 10
}

シャドーイングは控えめに使うべきだ。コンパイラは受け入れるが、内側の再宣言を見落とした読み手は x がどちらの値を保持しているか混乱しやすい。値が別の概念を表すなら、別の名前を付けることを優先しよう。

ブロック式(block expression)

ここでZigのブロック設計が多くの言語と異なる点が現れる。Zigでは、ブロックは値を生み出すことができ、式が書ける場所ならどこでも使える。そのためには:

  1. ブロックにラベルを付ける — 開き波括弧の前にコロンを伴う名前を書く:label: { ... }
  2. ブロック内で break :label value; を使ってブロックを抜け出し、value をブロックの結果として返す。
const std = @import("std");

pub fn main() void {
    const x: i32 = blk: {
        const a: i32 = 6;
        const b: i32 = 7;
        break :blk a * b; // このブロックは 42 と評価される
    };

    std.debug.print("x = {}\n", .{x}); // x = 42
}

ブロック式の型は break に渡した値から推論される。ここでは a * bi32 なので xi32 になる。ラベル名(この例では blk)は任意 — 意図を伝えるものを選ぼう。

これが重要な理由:一時変数の削減

ブロックが値を生み出せない言語では、複数のステップが必要な値を計算する際にミュータブルな中間変数を導入せざるを得ない:

// ブロック式なし — var を使わざるを得ない
var result: i32 = 0;
if (some_condition) {
    result = compute_a();
} else {
    result = compute_b();
}

ラベル付きブロックを使えば result をイミュータブルに保てる:

// ブロック式あり — result は const のまま
const result: i32 = blk: {
    if (some_condition) break :blk compute_a();
    break :blk compute_b();
};

値が変わらないなら const が常に望ましい。ブロック式を使えば、そうでなければ var が必要な状況でも const を選べる。

ラベル付きブロックと早期脱出

break :label はブロックの最後の文に限られない。ラベル付きブロックのどこからでも break できるため、複雑な初期化ロジックを早期終了するクリーンな手段として機能する:

const std = @import("std");

pub fn main() void {
    const input: i32 = -5;

    const clamped: i32 = clamp: {
        if (input < 0) break :clamp 0;
        if (input > 100) break :clamp 100;
        break :clamp input;
    };

    std.debug.print("clamped = {}\n", .{clamped}); // clamped = 0
}

break :clamp はブロックを抜け出してその値を提供する。最初に成立した条件が優先され、それ以降の行には到達しない。これは意図的で読みやすいパターンだ — 関数の早期リターンに似ているが、単一の式にスコープされている。

ネストされたブロック

ブロックは何層でもネストできる。各レベルが独自のスコープを導入し、break :label は常に指定したラベルを対象にする — 単に最も内側のブロックではなく:

const std = @import("std");

pub fn main() void {
    const value: i32 = outer: {
        const a: i32 = 3;

        const inner_result: i32 = inner: {
            const b: i32 = 4;
            break :inner a + b; // 内側のブロックを抜け出し、7 を返す
        };

        // inner_result は 7; 'b' はすでに消えている
        break :outer inner_result * 2; // 外側のブロックを抜け出し、14 を返す
    };

    std.debug.print("value = {}\n", .{value}); // value = 14
}

内側のブロックが終わると b は破棄される。外側のブロックはそれでも inner_result を使える。なぜなら、その変数は外側のスコープで宣言されているからだ。

言語全体でのブロック

ブロックは孤立した機能ではない。文の集まりを保持する構造的な要素として、Zigのあちこちに現れる:

  • 関数の本体fn name(...) ReturnType の後ろのブロックが関数の本体ブロックだ。関数が値を返すには、最終式に到達するか return を使う(break ではない)。
  • if / else if / else ブランチ — 各ブランチの本体はブロックで、if 構文全体もブロック式になれる(次のチェックポイントで説明)。
  • whilefor のループ本体 — ループで繰り返される部分はブロックで、break でループを抜け出せ、ラベル付きループも値を返せる。

これらの構文はそれぞれ独自のチェックポイントで説明される。すべてが同じブロックとスコープの規則に従うと認識すれば、新しい構文を理解しやすくなる。

まとめ

  • コードブロック{ } で囲まれた文の列であり、スコープを定義する。
  • ブロック内で宣言された変数はブロックが終わると破棄される。ブロックの外から見ることはできない。
  • 内側のスコープは外側のスコープの変数を読める;外側のスコープは内側のスコープを見ることができない。
  • シャドーイング — 外側のスコープから名前を再宣言すること — は許可されているが、慎重に使うべきだ。
  • ブロックはラベル(label: { ... })を付け、break :label value; で抜け出すことでブロック式になる。ブロックはその値と評価される。
  • ブロック式を使えば、複数のステップが必要なロジックで const 変数を初期化でき、一時的な var が不要になる。
  • break :label はラベル付きブロックのどこからでも書けて、クリーンな早期脱出パターンを提供する。
  • 関数の本体、if ブランチ、ループの本体はすべて同じスコープ規則に従うブロックだ。