Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得
LINQ 使用非常方便且彈性,而能讓他有如此的彈性需歸功於利用大量的委派;讓用戶端客制化自己的 Query 方式。而因為委派方法內有儲存“變數參考”時,當不經意的去修改該參考時,可能會出現讓人出乎意料的結果;本節將介紹其中原理。
考慮以下程式碼:
public IEnumerable<int> Generate(int max, Func<int> func)
{
var counter = 0;
while (counter < max)
{
yield return func();
counter++;
}
}
測試程式碼:
public void Test()
{
var index = 0;
Func<IEnumerable<int>> sequence = () => Generate(5,() => index++);
index = 20;
foreach (var n in sequence.Invoke())
Debug.WriteLine(n.ToString());
Debug.WriteLine("Done");
index = 100;
foreach (var n in sequence.Invoke())
Debug.WriteLine(n.ToString());
}
輸出會是:
20
21
22
23
24
Done
100
101
102
103
104
設定 index = 100 時,也同時修改到了委派內 index 的值;原因就是先前所提到,委派內儲存的是 index 的參考位置。
編譯器其實在背後幫我們做了一些事情,來看下面這個簡單例子。
int[] someNumbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var answers = from n in someNumbers
select n * n;
編譯器會產生類似以下程式碼:
private static int HiddenFunc(int n) => n * n;
private static Func<int, int> HiddenDelegateDefinition;
int[] someNumbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
if (HiddenDelegateDefinition == null)
HiddenDelegateDefinition = new Func<int, int>(HiddenFunc);
var answers = someNumbers.Select<int, int>(HiddenDelegateDefinition);
編譯器幫我們在背景產生了一組供 Select Query 使用的方法委派與方法 pair。
接著我們加入一個 Query 表達式。
class ModFilter
{
private readonly int _modulus;
public ModFilter(int mod)
{
_modulus = mod;
}
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
return from n in sequence
where n % _modulus == 0 // 新的條件表達式
select n * n; // 原先的選取表達式
}
}
編譯器產生的程式碼:
class ModFilterCompilie
{
private readonly int _modulus;
public ModFilterCompilie(int mod)
{
_modulus = mod;
}
// 新增的方法
private bool WhereClause(int n)
=> n % _modulus == 0;
// 原先的方法
private static int SelectClause(int n)
=> n * n;
// 原先的委派
private static Func<int, int> SelectDelegate;
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
if (SelectDelegate == null)
SelectDelegate = new Func<int, int>(SelectClause);
return sequence.Where(new Func<int, bool>(WhereClause))
.Select<int, int>(SelectClause);
}
}
我們可以看到,在 LINQ 語法糖的背後,都只是委派與方法的宣告;只是外部不需要關心這些就可以簡單使用。
那麼,加入變數的情況會是如何呢?
再次修改程式碼:
class ModFilter2
{
private readonly int _modulus;
public ModFilter2(int mod)
{
_modulus = mod;
}
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
int numValues = 0;
return from n in sequence
where n % _modulus == 0
select n * n / ++numValues; // 新增了 numValues
}
}
編譯器產生的程式碼:
class ModFilter2Compile
{
private sealed class Closure
{
public ModFilter2Compile _outer;
public int _numValues;
public int SelectClause(int n)
=> (n * n) / ++_numValues;
}
private readonly int _modulus;
public ModFilter2Compile(int mod)
{
_modulus = mod;
}
private bool WhereClause(int n)
=> (n % _modulus) == 0;
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
var c = new Closure
{
_outer = this,
_numValues = 0
};
return sequence.Where<int>(new Func<int, bool>(WhereClause))
.Select<int, int>(new Func<int, int>(c.SelectClause));
}
}
可以看到,編譯器幫我們自動產生了 Closure 類別,用以儲存 _numberValue 與 ModFilter2Compile 本身,還記得 Item41 提過的 IEnumerable 延遲執行的特性,會造成物件生命週期延長嗎?只有在外部不再參考 FindValues 回傳結果時,Closure 與 ModFilter2Compile 資源才會被釋放。聽起來很抽象,先跑個測試來看看就知道了。
測試程式碼:
var list = new List<int>{6,3};
var x = new ModFilter2Compile(3);
var y = x.FindValues(list);
foreach (var e in y)
Debug.WriteLine(e.ToString());
Debug.WriteLine("Done");
foreach (var e in y)
Debug.WriteLine(e.ToString());
經過兩次繞行的結果將不會是:
36
4
Done
36
4
而是:
36
4
Done
12
2
在一開始的例子中,此處的 _numValues 就相當於 index,當外部或內部去修改該值時,修改的不會是副本,而是值本身。
1. 使用 LINQ 時,需注意外部是否有去修改到委派方法內參考到的值;否則容易發生未預期的行為。