CodeGym /課程 /C# SELF /區域函式的變數捕捉

區域函式的變數捕捉

C# SELF
等級 11 , 課堂 5
開放

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();
}

這裡 prefixmessage 這兩個變數在區域函式 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
Closure: count 變數在方法結束後還活著

這裡,即使 GetCounter 方法已經結束,count 變數還是活著,因為被傳出去的函式捕捉住了它。這就叫closure(閉包)——之後還會有專門的課講這個,但區域函式的機制也是一樣的。

6. 常見錯誤和有趣的情境

在呼叫區域函式前改變變數

有時候你會遇到這種情況:區域函式捕捉的變數,在呼叫前被改變了——結果可能跟你想的不一樣。

例子:


void Example()
{
    int x = 42;
    void PrintX() {  Console.WriteLine(x);  } 

    x = 100; // 變數被改了!
    PrintX(); // 會印出 100,不是 42!
}

小技巧:區域函式永遠看到的是呼叫時變數的最新值。

在 for/foreach 迴圈裡捕捉變數(再說一次)

經典大坑:不管你是在面試還是大專案裡寫邏輯,都要檢查一下:我是不是捕捉了「活著」的迴圈變數?它會怎麼表現?

1
問卷/小測驗
Tuple 和本地函式,等級 11,課堂 5
未開放
Tuple 和本地函式
Tuple 比 out 跟匿名型別更讚的地方
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION