這個做法在解說 closure 把區域變數提升為成內部成員時,又被其他程式碼修改所導致的問題。

首先看看這個範例,你可能會覺得結果會輸出兩次 0 ~ 30,但實際上輸出的是 20 ~ 50 和 100 ~ 130。

void Main()
{
	var index = 0;
	Func<IEnumerable<int>> sequence = () => Utilities.Generate(30, () => index++);
	
	index = 20;
	foreach (int n in sequence())
		Console.WriteLine(n);
		
	Console.WriteLine("Done");
	index = 100;
	foreach (var n in sequence())
		Console.WriteLine(n);
}

public static class Utilities
{
	public static IEnumerable<int> Generate(int max, Func<int> func)
	{
		for (int i = 0; i <= max; i++)
		{
			yield return func();
		}
	}
}

會有這樣的結果是編譯器在背後做了不少的工作,要了解背後的原理可以先從下面這個簡單的例子進行學習。

int[] someNumbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var answers = from n in someNumbers
                select n * n;

編譯器會生成出類似這樣的程式碼,關鍵就是之前提到的 LINQ to Objects 背後所產生的 Static Delegate。

internal int HiddenFunc(int n) => (n * n);
private static Func<int, int> HiddenDelegateDefinition;

int[] someNumbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

if (HiddenDelegateDefinition == null)
{
    HiddenDelegateDefinition  = new Func<int, int>(HiddenFunc);
}

var answers = someNumbers.Select<int, int>(HiddenDelegateDefinition);

接下來看一下另一個例子,此例中寫的 lambda 語句需要讀取 instance variables 也就是 modulus 的值才能計算出結果。

public 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 // New expression
			   select n * n; // previous example
	}
}

編譯轉換之後會長的像下面這樣,也是產生 Static Delegate 不過這次多產生了 WhereClause 私有方法,最後也還是透過 Delegate 串接的特性 將兩個 Delegate 串接起來產生結果。

public class ModFilter
{
	private readonly int modulus;
	private bool WhereClause(int n) => ((n % this.modulus) == 0);
	internal int SelectClause(int n) => (n * n);
	private static Func<int, int> SelectDelegate;
	
	public ModFilter(int mod)
	{
		modulus = mod;
	}
	
	public IEnumerable<int> FindValues(IEnumerable<int> sequence)
	{
		if (SelectDelegate == null)
		{
			SelectDelegate = new Func<int, int>(SelectClause);
		}
		
		return sequence.Where<int>(
			new Func<int, bool>(this.WhereClause)).
			Select<int, int>(SelectDelegate);
	}
}

最後看一下複雜一點的例子,FindValues 方法還額外使用了一個區域變數 numValues,這樣會做會產生一個 closure。

public class ModFilter
{
	private readonly int modulus;
	public ModFilter(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;
	}
}

所以這個時候內部就會產生嵌套的類別 Closure,它的用意是把 lambda 會訪問或修改的所有變數都加進來並轉換成 field。

public class ModFilter
{
	private sealed class Closure
	{
		public ModFilter outer;
		public int numValues;
		public int SelectClause(int n) => ((n * n) / ++this.numValues);
	}
	private readonly int modulus;
	public ModFilter(int mod)
	{
		this.modulus = mod;
	}
	private bool WhereClause(int n) => ((n % this.modulus) == 0);
	
	public IEnumerable<int> FindValues(IEnumerable<int> sequence)
	{
		var c = new Closure();
		c.outer = this;
		c.numValues = 0;
		return sequence.Where<int>
			(new Func<int, bool>(this.WhereClause))
			.Select<int, int>(new Func<int, int>(c.SelectClause));
	}
}

看過這三個案例後就可以知道為什麼開頭問題會是這樣的結果,是因為 index 區域變數被加入到了內部隱藏的 Closure 類別裡面, 實際上會長的如下面這樣,不難看出其實我們都是在操作同一個 instance 底下的 field 而已。

void Main()
{
	Closure c = new Closure();
	c.index = 0;
	Func<IEnumerable<int>> func = new Func<IEnumerable<int>>(GenerateClause);
	c.index = 20;
	IEnumerator<int> enumerator = func().GetEnumerator();
	Console.WriteLine("Done");
	c.index = 100;
	IEnumerator<int> enumerator2 = func().GetEnumerator();
}

private sealed class Closure
{
	public int index;
	public Func<int> GenerateDelegate;
	internal IEnumerable<int> indexPlus => ++this.index;
	internal IEnumerable<int> GenerateClause => return Utilities.Generate(30, indexPlus);
}

Summary

這個做法在介紹 closure 是如何處理捕獲的變數的,一種是轉換成欄位成員另一種則是再透過一個新的嵌套類別把欄位成員放在裡面, 由於這種操作導致程式運作的時候發生奇怪的錯誤,所以建議是不要去修改捕獲的變數是比較好的做法。