C# 定義數值 Call By Value,物件 Call By Reference,在使用 Lambda Expression 傳參數時,變數是 "參考指標",可能會與預期不同
學 C# 的第一天起,就深植了這個印像:參數若是數值是 Call By Value,
若是物件則是 Call By Reference。
較精確的說法:
C# 的參數有數種傳遞方式,包含傳值參數 (call by value),傳址參數 (call by reference) 等,基本型態的參數,像是 int, double, char, … 等,預設都使用傳值的方式,而物件形態的參數,像是 StringBuilder,陣列等,預設則是使用傳址的方式。
以上摘錄自: http://cs0.wikidot.com/function
筆者遇到一個看似推翻上述的案例,使用 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
筆者猜測在執行時期,有可能是透過此類別做為傳遞,
因此仍不違反 "數值是 Call By Value,若是物件則是 Call By Reference" 的說法。