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)
變數捕捉 就是區域函式用到那些不是在自己裡面宣告、但在同一個作用域的變數。
區域函式和匿名方法(lambda 表達式,這個之後會講)會記住(或「捕捉」)它們宣告時能用到的所有變數。
可以說,函式好像拍了一張「快照」(capture),把外面的世界記下來——即使很久以後才被呼叫,也能用這些變數。
示意圖
Main 方法
└─ 變數 x
└─ 區域函式 F() ←「捕捉」x
例子——最簡單的捕捉
void CounterExample()
{
int counter = 0;
void Increase()
{
counter++; // 這個函式捕捉了 counter 變數
}
Increase();
Increase();
Console.WriteLine(counter); // 會印出 2
}
這裡呼叫兩次區域函式 Increase 之後,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. 區域函式和變數在方法結束後還活著嗎?
變數會活得比方法久嗎?
如果你把區域函式(或帶 lambda 的 delegate)傳到方法外面,捕捉到的變數就不會在方法結束時「死掉」。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