Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得
前一篇提到了物件會因為延遲執行與委派,進而延長生命週期的情況。這篇繼續用一些實際的例子來說明。
假設有以下方法:
public static class Extensions
{
public static IEnumerable<string> ReadLines(this TextReader reader)
{
var txt = reader.ReadLine();
while (txt != null)
{
yield return txt;
txt = reader.ReadLine();
}
}
public static int DefaultParse(this string input, int defaultValue)
{
return int.TryParse(input, out var answer) ? answer : defaultValue;
}
}
// 讀取目標檔案,逐行讀取字串後,用 ',' 分隔各個子字串;接著將每一個陣列元素轉換成整數。
public static IEnumerable<IEnumerable<int>> ReadNumbersFromStream(TextReader t)
{
var allLines = from line in t.ReadLines()
select line.Split(',');
var matrixOfValues = from line in allLines
select from item in line
select item.DefaultParse(0);
return matrixOfValues;
}
客戶端程式碼:
var reader = new StreamReader(File.OpenRead("TestFile.txt"));
var rowOfNumbers = ReadNumbersFromStream(reader);
接著,在下一次 Code Review 會被說:嘿~你忘記釋放資源了喔。
於是很自然的改成以下程式碼:
var rowOfNumbers = default(IEnumerable<IEnumerable<int>>);
using (var reader = new StreamReader(File.OpenRead("TestFile.txt")))
rowOfNumbers = ReadNumbersFromStream(reader);
嗯,看起來很不錯;有用 using 釋放資源。但跑測試後會擲出 ObjectDisposedException 例外。
var rowOfNumbers = default(IEnumerable<IEnumerable<int>>);
using (var reader = new StreamReader(File.OpenRead("TestFile.txt")))
rowOfNumbers = ReadNumbersFromStream(reader);
// 擲出例外
foreach (var line in rowOfNumbers)
{
foreach (var num in line)
Debug.WriteLine($"Number is {num.ToString()}");
}
原因還是先前提過的,延遲執行。IEnumerable 只有在呼叫 foreach 或 ToList 時,才會真正執行。於是又修改了程式:
using (var reader = new StreamReader(File.OpenRead("TestFile.txt")))
{
var rowOfNumbers = ReadNumbersFromStream(reader);
foreach (var line in rowOfNumbers)
foreach (var num in line)
Debug.WriteLine($"Number is {num.ToString()}");
}
那就把資源釋放跟用戶端程式碼包在一起就好啦,但這樣有個缺點;重複的程式碼多了(客戶端每次呼叫都要記得 using)。
那要不然把 using 區塊封裝到底層如何呢?
public static IEnumerable<IEnumerable<int>> ReadNumbersFromStream(string path)
{
using (var reader = new StreamReader(File.OpenRead(path)))
{
var allLines = from line in reader.ReadLines()
select line.Split(',');
var matrixOfValues = from line in allLines
select from item in line
select item.DefaultParse(0);
return matrixOfValues;
}
}
再進一步,把客戶端對集合的操作寫成委派,讓 using 區塊真正的留在底層方法;去除外部需要多去管裡 StreamReader 的情況。
// 定義客戶端方法委派
public delegate TResult ProcessElementsFromFile<TResult>(IEnumerable<IEnumerable<int>> values);
// 定義包含讀取與依照外部傳入委派執行的方法
public static TResult ProcessFile<TResult>(string filePath, ProcessElementsFromFile<TResult> func)
{
using (var reader = new StreamReader(File.OpenRead(path)))
{
var allLines = from line in reader.ReadLines()
select line.Split(',');
var matrixOfValues = from line in allLines
select from item in line
select item.DefaultParse(0);
return func(matrixOfValues);
}
}
客戶端程式碼:
var maximum = ProcessFile("Test.txt", arrayOfNums=>
(from line in arrayOfNums
select line.Max()).Max());
如此一來,我們已經將檔案開啟讀取與釋放完全和外部程式碼解耦;不需再去擔心物件的生命週期。
結論:
1. 物件的生命週期,即便在記憶體託管的環境下;仍然需要開發者去留意。
2. 延遲執行造成物件生命週期延展;可能的話,盡早呼叫 ToList 把結果存入快取中也是一個方式。在此例的方式為:
1. 物件的生命週期,即便在記憶體託管的環境下;仍然需要開發者去留意。
2. 延遲執行造成物件生命週期延展;可能的話,盡早呼叫 ToList 把結果存入快取中也是一個方式。在此例的方式為:
// 呼叫 ToList 立即執行,不再需要 StreamReader。
var allLines = (from line in reader.ReadLines()
select line.Split(',')).ToList();