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 解除參考時,上述物件才得以回收。
在了解原理後,下一篇接著討論一個比較實際的例子。