Chapter 4 - Item 41 : Avoid Capturing Expensive Resources # 1

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

寫程式需要在意的很重要一點是物件的生命週期,縱使在 C# 語言提供強大的 GC Collect 機制下;開發者也需要注意資源是否有正確釋放(如 unmanage resource, e.g. Socket DbConneciton, FileAccess)。除了 unmanage resource 外,還有一些情況是需要注意的,也就是本節要討論的。

一般我們會認為在 Call Stack 的架構下,在離開方法區塊後,其中宣告的區域變數會被視為不可到達(unreachable);隨即 pop out。若是值型別則立刻清空儲存在 Stack 中的值;若是參考型別,除了清空 Stack 中儲存的參考外,還需等待 GC 介入進而回收 Heap 中的記憶體垃圾。

然而這只是一個概括的輪廓,並不代表百分之百會按照如此運作,還是要看程式是如何撰寫。
以下我們看一個延長物件生命週期的例子。

var counter = 0;
var numbers = Generate(30,() => counter++);

public IEnumerable<int> Generate(int max, Func<int> func)
{
	var counter = func();
	while (counter < max)
	{
		yield return counter;
		counter = func();
	}
}

編譯器會產生類似以下的程式碼:

class Closure
{
	public int generatedCounter;
	public int generatorFunc() => generatedCounter++;
}

實際上會像是這樣一段程式碼:

var c = new Closure();
c.generatedCounter = 0;
var sequence = Generate(30, new Func<int>(c.generatorFunc));
return sequence;

假設客戶端的 API 如以下程式碼:

public IEnumerable<int> MakeSequence()
{
	var counter = 0;
	var numbers = Generate(30, () => counter++);
	return numbers;
}

基於先前的邏輯,編譯器對該 API 自動產生的程式碼會是:

public IEnumerable<int> MakeSequence()
{
	var c = new Closure();
	c.generatedCounter = 0;
	var sequence = Generate(30, new Func<int>(c.generatorFunc));
	return sequence;
}

由於 IEnumerable 的特性在於延遲執行,只有在外部呼叫 foreach 或是呼叫 ToList 時,才會“真正”呼叫 Generate 方法。仔細觀察 MakeSequence 這個看似簡單的方法,為了要達到延遲執行的這項特性;即便已經 return sequence,產生 sequence 需要委派 Func<int>,而為了得到委派 Func<int> 需要 Closure gnerateFunc 委派成員,為了得到 generateFunc 委派,則需要保留整個 Closure 物件。換句話說,該區塊的所有物件都被保留了,並非離開方法區塊後被視為垃圾;只有在外部對 sequence 解除參考時,上述物件才得以回收。

在了解原理後,下一篇接著討論一個比較實際的例子。