Call By Value 與 Call By Reference

學 C# 的第一天起,就深植了這個印像:參數若是數值是 Call By Value,
若是物件則是 Call By Reference。
較精確的說法:

C# 的參數有數種傳遞方式,包含傳值參數 (call by value),傳址參數 (call by reference) 等,基本型態的參數,像是 int, double, char, … 等,預設都使用傳值的方式,而物件形態的參數,像是 StringBuilder,陣列等,預設則是使用傳址的方式。

以上摘錄自: http://cs0.wikidot.com/function
But, 筆者遇到一個看似推翻上述的案例,使用 Lambda Expressions 傳入數值,竟然是以 Call By Reference 方式。
發生不如預期的程式碼如下
public void Test(int numSites)
{
    System.Timers.Timer[] scanLiveTimer = new System.Timers.Timer[numSites];

    for (int si = 0; si < numSites; si++)
    {
        scanLiveTimer[si] = new System.Timers.Timer();

        scanLiveTimer[si].Elapsed +=
            new System.Timers.ElapsedEventHandler((sender, e) => ScanMarketEvent(si));

        scanLiveTimer[si].Interval = 500;
        scanLiveTimer[si].Start();
        Console.WriteLine(string.Format("Trigger: {0}", si));
    }
}

void ScanMarketEvent(int si)
{
    // 有誤,讀到的值都一樣
    Console.Write(string.Format("{0} ", si));
}


在 第 10 行明確傳入 int 數值,但執行結果是等於 si 在迴圈後的值


---> 這看起是 Call By Reference 的結果





經過小調整後,在使用 Lambda Expressions 先 複製 成另一個數值再傳入,執行可以得到預期結果


public void Test(int numSites)
{
    System.Timers.Timer[] scanLiveTimer = new System.Timers.Timer[numSites];

    for (int si = 0; si < numSites; si++)
    {
        scanLiveTimer[si] = new System.Timers.Timer();
        // 複製變數,再傳入 lambda expression
        int x = si;

        scanLiveTimer[si].Elapsed +=
            new System.Timers.ElapsedEventHandler((sender, e) => ScanMarketEvent(x));

        scanLiveTimer[si].Interval = 500;
        scanLiveTimer[si].Start();
        Console.WriteLine(string.Format("Trigger: {0}", si));
    }
}

void ScanMarketEvent(int si)
{
    // 可以讀到正確的值
    Console.Write(string.Format("{0} ", si));
}





使用反組譯工具發現,使用 Lambda Expressions 額外多了一個類別 c__DisplayClass2


image





筆者猜測在執行時期,有可能是透過此類別做為傳遞,


因此仍不違反 "數值是 Call By Value,若是物件則是 Call By Reference" 的說法。





測試案例:下載原始檔  or 直接瀏覽Gist