コンテンツにスキップ

Rustのスコープ

スコープ(scope)とは、変数が有効なコードの範囲のことです。考え方はシンプルですが、これを理解しておくと、Rustのほかの多くのルール(特に所有権)がすっと腑に落ちるようになります。

変数は、宣言された場所から、その変数があるブロックの閉じかっこ } までの間だけ存在します。

fn main() {
let x = 5; // ここで x がスコープに入る
{
let y = 10; // ここで y がスコープに入る
println!("x = {x}, y = {y}"); // どちらも有効
} // ここで y がスコープを抜ける
println!("x = {x}");
// println!("{y}"); // ❌ エラー: y はもうスコープにない
}

実行結果:

x = 5, y = 10
x = 5
▶ 実際に試してみよう

y が存在するのは、内側の { } ブロックの中だけです。そのブロックが終わると y はなくなります。その後で使おうとすると、実行時に思いがけず失敗するのではなく、コンパイルエラーになります。

スコープは一方通行です。内側のブロックからは、それを囲むブロックの変数が見えます(だから内側のブロックで x を表示できます)。でも、外側のブロックから内側をのぞくことはできません。

関数の引数や、関数の中で作った変数は、その関数が呼び出されている間だけ存在します。

fn calculate() -> i32 {
let result = 5 * 2; // result は calculate の中にしか存在しない
result
}
fn main() {
let value = calculate();
println!("{value}");
// println!("{result}"); // ❌ エラー: ここには result は存在しない
}

実行結果:

10
▶ 実際に試してみよう

同じことは逆向きにも言えます。関数からは、main のローカル変数は見えません。値が必要なら、関数のページで見たように、引数として渡します。

{ } ブロックは、変数が生きる範囲を区切るだけではありません。値を生み出すこともできます。その値は、ブロックの中の最後の式(セミコロンなし)です。関数が最後の行を返すのと同じルールです。

何かを計算するのに補助の変数がいくつか必要だけれど、計算が終わった後まで残しておきたくない。そんなときに便利です。

fn main() {
let price = 40;
let total = {
let tax = price / 10; // tax はこのブロックの中だけに存在する
let shipping = 5;
price + tax + shipping // セミコロンなし:これがブロックの値になる
};
println!("合計 = {total}");
// println!("{tax}"); // ❌ エラー: tax はスコープの外
}

実行結果:

合計 = 49
▶ 実際に試してみよう

気をつけたい点が2つあります。

  • 最後の行 price + tax + shipping にはセミコロンがありません。セミコロンを付けると、ブロックは数値ではなく ()(何もない値)を返すようになり、println! で表示できなくなります。
  • 全体が1つの let 文なので、閉じかっこの後ろは }; で終わります。

ブロックが終わると、tax と shipping はなくなり、結果の total だけが残ります。補助の変数をこうした小さなブロックに入れておくと、それらが1つの計算のためだけに必要なのだと、はっきり伝わります。

スコープが所有権にとって大切な理由

Section titled “スコープが所有権にとって大切な理由”

これは、少し後で学ぶ所有権と直接つながっています。値は、その所有者のスコープが終わった瞬間に、自動的に片付けられるのです。

fn main() {
{
let text = String::from("こんにちは");
println!("{text}");
} // text のスコープはここで終わる — Rustが自動的に破棄(drop)する
}

実行結果:

こんにちは
▶ 実際に試してみよう

Rustにガベージコレクタが要らない仕組みは、これに尽きます。どのスコープがどこで終わるのかを、コンパイルの時点で正確に把握しているのです。

変数のページで学んだシャドーイングを覚えていますか? スコープの中で同じ名前を使って let を書くたびに、別の変数が作られます。外側の変数を消すわけではなく、そのスコープの残りの部分でだけ置き換わるのです。

fn main() {
let x = 5;
{
let x = x * 2; // 新しい x。このブロックの中だけ
println!("内側の x = {x}"); // 10
}
println!("外側の x = {x}"); // 5 — 影響を受けない
}

実行結果:

内側の x = 10
外側の x = 5
▶ 実際に試してみよう

次のプログラムはコンパイルできません。area を、そのブロックが終わった後で使っているからです。

fn main() {
let width = 3;
{
let height = 4;
let area = width * height;
}
println!("面積 = {area}");
}

height はブロックの中だけに置いたまま、面積 = 12 と表示されるように直してください。ヒント:ブロックが面積を生み出すようにして、その結果をブロックの外で宣言した変数に入れます。

解答を見る
fn main() {
let width = 3;
let area = {
let height = 4;
width * height
};
println!("面積 = {area}");
}

実行結果:

面積 = 12
▶ 実際に試してみよう

これで area は main のスコープにあるので、println! から見えます。height は相変わらずブロックの中でしか必要なく、ブロックが終わると破棄されます。

level という変数を1で作ります。ブロックを開いて2でシャドーイングし、その中でさらにブロックを開いて3でシャドーイングしてください。各ブロックの中で level を表示し、それぞれのブロックが閉じた直後にもう一度表示します。ヒント:数字は1、2、3と上がり、その後2、1と戻っていくはずです。

解答を見る
fn main() {
let level = 1;
println!("レベル{level}");
{
let level = 2;
println!("レベル{level}");
{
let level = 3;
println!("レベル{level}");
}
println!("レベル{level}に戻った");
}
println!("レベル{level}に戻った");
}

実行結果:

レベル1
レベル2
レベル3
レベル2に戻った
レベル1に戻った
▶ 実際に試してみよう

内側の level が外側のものを隠すのは、そのブロックが終わるまでだけです。外側の変数は一度も変更されていないので、ちゃんとそこで待っています。

  • スコープとは、変数が有効な範囲({ } で区切られる)のことです。
  • 変数は、スコープの閉じかっこ } に達した瞬間に存在しなくなります。
  • 関数の本体もスコープです。関数の中のローカル変数は、関数から戻るときに消えます。
  • ブロックは式です。最後の行(セミコロンなし)がブロックの値になり、補助の変数はブロックが終わると消えます。
  • スコープによって、Rustは値を破棄するタイミングを正確に知ることができます。だからガベージコレクタが要らないのです。

理解度チェック

1. このページによると、Rustにガベージコレクタが要らないのはなぜですか?

2. 内側のブロックで let x = x * 2; と書いて変数をシャドーイングすると、どうなりますか?

3. let total = { let a = 2; a * 10 }; を実行した後、total の値は何ですか?

スコア: 0 / 3