Rustのスコープ
スコープ(scope)とは、変数が有効なコードの範囲のことです。考え方はシンプルですが、これを理解しておくと、Rustのほかの多くのルール(特に所有権)がすっと腑に落ちるようになります。
スコープは波かっこで決まる
Section titled “スコープは波かっこで決まる”変数は、宣言された場所から、その変数があるブロックの閉じかっこ } までの間だけ存在します。
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 = 10x = 5y が存在するのは、内側の { } ブロックの中だけです。そのブロックが終わると y はなくなります。その後で使おうとすると、実行時に思いがけず失敗するのではなく、コンパイルエラーになります。
スコープは一方通行です。内側のブロックからは、それを囲むブロックの変数が見えます(だから内側のブロックで x を表示できます)。でも、外側のブロックから内側をのぞくことはできません。
関数の本体もスコープ
Section titled “関数の本体もスコープ”関数の引数や、関数の中で作った変数は、その関数が呼び出されている間だけ存在します。
fn calculate() -> i32 { let result = 5 * 2; // result は calculate の中にしか存在しない result}
fn main() { let value = calculate(); println!("{value}"); // println!("{result}"); // ❌ エラー: ここには result は存在しない}実行結果:
10同じことは逆向きにも言えます。関数からは、main のローカル変数は見えません。値が必要なら、関数のページで見たように、引数として渡します。
ブロックも式になる
Section titled “ブロックも式になる”{ } ブロックは、変数が生きる範囲を区切るだけではありません。値を生み出すこともできます。その値は、ブロックの中の最後の式(セミコロンなし)です。関数が最後の行を返すのと同じルールです。
何かを計算するのに補助の変数がいくつか必要だけれど、計算が終わった後まで残しておきたくない。そんなときに便利です。
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にガベージコレクタが要らない仕組みは、これに尽きます。どのスコープがどこで終わるのかを、コンパイルの時点で正確に把握しているのです。
シャドーイングとスコープ
Section titled “シャドーイングとスコープ”変数のページで学んだシャドーイングを覚えていますか? スコープの中で同じ名前を使って let を書くたびに、別の変数が作られます。外側の変数を消すわけではなく、そのスコープの残りの部分でだけ置き換わるのです。
fn main() { let x = 5; { let x = x * 2; // 新しい x。このブロックの中だけ println!("内側の x = {x}"); // 10 } println!("外側の x = {x}"); // 5 — 影響を受けない}実行結果:
内側の x = 10外側の x = 51. スコープのエラーを直す
Section titled “1. スコープのエラーを直す”次のプログラムはコンパイルできません。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 は相変わらずブロックの中でしか必要なく、ブロックが終わると破棄されます。
2. 入れ子の階層
Section titled “2. 入れ子の階層”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は値を破棄するタイミングを正確に知ることができます。だからガベージコレクタが要らないのです。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. このページによると、Rustにガベージコレクタが要らないのはなぜですか?
2. 内側のブロックで let x = x * 2; と書いて変数をシャドーイングすると、どうなりますか?
3. let total = { let a = 2; a * 10 }; を実行した後、total の値は何ですか?
スコア: 0 / 3
さらに詳しく
Section titled “さらに詳しく”- 変数スコープ — The Rust Programming Language 日本語版:所有権への第一歩として、スコープの仕組みを説明しています。
- スコープとシャドーイング — Rust By Example 日本語版:ブロックとシャドーイングの短い実行できるサンプルです。
- RAII — Rust By Example 日本語版:スコープが終わったときに値がどうなるかを説明しています。次の一歩に進む準備ができたらどうぞ。
- Block expressions — The Rust Reference(英語):ブロックと、ブロックが生み出す値についての正確なルールです。