1. はじめに
スコープを思い出そう
変数っていうのは大きなオフィスの社員みたいなもので、メソッドやループ、コードブロックは部屋やオフィスみたいなもんだと思ってみて。ある社員は自分の部屋だけに入れるし、他の社員はビル全体に入れる。誰がどこにいられるか――それがスコープ(scope)だよ。
スコープは、宣言された変数がプログラムのどこで「見える」か、どこで使えるかを決めてるんだ。
主なスコープの種類
C#では、こんな主なスコープがあるよ:
| スコープ | 例 | 変数が「見える」場所 |
|---|---|---|
| ローカル | メソッドやブロックの中 | そのブロックの中だけ |
| メソッドのパラメータ | メソッドのシグネチャ内 | メソッドの中だけ |
| クラスの変数(フィールド) | メソッド外のクラス本体 | そのクラスの全メソッドで |
| ループ/条件内の変数 | の中のループ/if |
その の中だけ |
説明付きの例
public class Office
{
int buildingNumber = 50; // クラスのフィールド:全メソッドで見える public void PrintInfo() {
int roomNumber = 101; // ローカル変数:PrintInfoの中だけ見える if (roomNumber > 100) {
int deskNumber = 5; // このifブロックの中だけ見える Console.WriteLine(deskNumber);
} Console.WriteLine(deskNumber); //
エラー! deskNumberはここでは見えない
}
}
2. ローカル関数とスコープ
誰が誰を「見える」?
メソッドの中(またはループや条件の中)でローカル関数を宣言すると、その関数は上で宣言された変数と同じスコープにいることになる。ローカル関数は「同じ部屋の一部」みたいな感じだね。
例
ローカル関数は周囲のスコープの変数が見える
void PrintWithPrefix(string message)
{
string
prefix = "[ログ]: "; void Print() {
Console.WriteLine(
prefix +
message); // 両方の変数が見える!
} Print();
}
ここでは、prefix と message の変数はローカル関数 Print の中で見える。なぜなら、同じかもっと広いスコープで宣言されてるからだよ。
スコープはいくつある?
上の例だと:
- PrintWithPrefix メソッドのスコープがある
- その中に Print 関数のスコープがある
3. 変数のキャプチャ(Capture)
変数のキャプチャ っていうのは、ローカル関数が自分の外で宣言された変数(でも同じスコープ内)を使うことだよ。
ローカル関数や匿名メソッド(ラムダ式、これは後でやるよ)は、宣言時に使えた変数を全部覚えて(「キャプチャ」して)おくんだ。
つまり、関数はまるで周りの世界のスナップショット(capture)を撮ってるみたいなもので、ずっと後で呼び出されたときでもその変数を使えるんだ。
図でイメージ
Mainメソッド
└─ 変数x
└─ ローカル関数F() ← xを「キャプチャ」する
例 — 一番シンプルなキャプチャ
void CounterExample()
{
int counter = 0;
void Increase()
{
counter++; // この関数はcounter変数をキャプチャしてる
}
Increase();
Increase();
Console.WriteLine(counter); // 2が出力される
}
ここではローカル関数 Increase を2回呼んだあと、counter の値は 2 になるよ。
4. 変数キャプチャの使い道
変数のキャプチャがあると、メソッドのスコープとローカル関数の間でデータを「渡す」のがすごく楽になる。余計なパラメータを使わなくていいんだ。
もしキャプチャがなかったら、全部の変数をパラメータで渡さなきゃいけなくなる:
void CounterExampleWithoutCapture()
{
int counter = 0;
void Increase(ref int c)
{
c++;
}
Increase(ref counter);
Increase(ref counter);
Console.WriteLine(counter);
}
これだと面倒だよね。毎回refって書かなきゃいけないし、関数のシグネチャもごちゃごちゃになる。外の変数が簡単に「見える」なら、その方がいいよね。
5. ローカル関数とメソッド終了後の変数の生存
変数はメソッドの外でも生きてる?
ローカル関数(やラムダ付きのデリゲート)を今のメソッドの外に渡すと、キャプチャされた変数はメソッドを抜けても「死ななく」なる。CLR(.NETの仮想マシン)がちゃんと面倒見てくれるから、必要なものはメモリに「しがみついて」残るんだ。
例:関数がメソッドの外で生きてる
Func<int> GetCounter()
{
int count = 0;
int Increment()
{
count++;
return count;
}
return Increment; // 関数を外に返してる!
}
var counter = GetCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
ここでは、GetCounter メソッドが終わった後でも、count 変数は生き続けてる。なぜなら返した関数がそれをキャプチャしてるから。これをクロージャ(closure)って呼ぶよ。これはまた別のレクチャーでやるけど、ローカル関数でも仕組みは同じだよ。
6. よくあるミスや面白いパターン
ローカル関数呼び出し前の変数の書き換え
たまに、ローカル関数がキャプチャしてる変数が呼び出し前に書き換えられてて、思ったのと違う結果になることがあるんだ。
例:
void Example()
{
int x = 42;
void PrintX() { Console.WriteLine(x); }
x = 100; // 変数を書き換えた!
PrintX(); // 100が出力される、42じゃない!
}
ポイント: ローカル関数は呼び出し時点での変数の最新値が見えるよ。
for/foreachループでの変数キャプチャ(もう一度)
定番の落とし穴:面接や大きなプロジェクトでロジックを書くときは、「ループの中の生きてる変数をキャプチャしてないか」「その変数がどう振る舞うか」を必ずチェックしよう。
GO TO FULL VERSION