コードブロック
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では、ブロックは値を生み出すことができ、式が書ける場所ならどこでも使える。そのためには:
- ブロックにラベルを付ける — 開き波括弧の前にコロンを伴う名前を書く:
label: { ... }。 - ブロック内で
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 * b が i32 なので x も i32 になる。ラベル名(この例では 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構文全体もブロック式になれる(次のチェックポイントで説明)。whileとforのループ本体 — ループで繰り返される部分はブロックで、breakでループを抜け出せ、ラベル付きループも値を返せる。
これらの構文はそれぞれ独自のチェックポイントで説明される。すべてが同じブロックとスコープの規則に従うと認識すれば、新しい構文を理解しやすくなる。
まとめ
- コードブロックは
{ }で囲まれた文の列であり、スコープを定義する。 - ブロック内で宣言された変数はブロックが終わると破棄される。ブロックの外から見ることはできない。
- 内側のスコープは外側のスコープの変数を読める;外側のスコープは内側のスコープを見ることができない。
- シャドーイング — 外側のスコープから名前を再宣言すること — は許可されているが、慎重に使うべきだ。
- ブロックはラベル(
label: { ... })を付け、break :label value;で抜け出すことでブロック式になる。ブロックはその値と評価される。 - ブロック式を使えば、複数のステップが必要なロジックで
const変数を初期化でき、一時的なvarが不要になる。 break :labelはラベル付きブロックのどこからでも書けて、クリーンな早期脱出パターンを提供する。- 関数の本体、
ifブランチ、ループの本体はすべて同じスコープ規則に従うブロックだ。