在做法 29 與做法 33 提到的 Lazy Evaluation 在這個做法又提到了一次,不過這次有提出相對的概念也就是 Eager Evaluation。 首先還是複習一下 Lazy Evaluation 的運作邏輯,下面這一段程式碼建立了一個 Iterator Methods Generate,當我們呼叫靜態方法 LazyEvaluation 的時候會產生三個 DateTime.Now,按下 ENTER 之後又會產生不同的三個 DateTime.Now。

void Main()
{
	LazyEvaluation();
}

private static IEnumerable<TResult> Generate<TResult>(int number, Func<TResult> generator)
{
	for (var i = 0; i < number; i++)
		yield return generator();
}
private static void LazyEvaluation()
{
	Console.WriteLine($"Start time for Test One: {DateTime.Now:T}");
	var sequence = Generate(3, () => DateTime.Now);
	Console.WriteLine("Waiting....\tPress Return");
	Console.ReadLine();
	Console.WriteLine("Iterating...");
	foreach (var value in sequence)
		Console.WriteLine($"{value:T}");
	Console.WriteLine("Waiting....\tPress Return");
	Console.ReadLine();
	Console.WriteLine("Iterating...");
	foreach (var value in sequence)
		Console.WriteLine($"{value:T}");
}
Start time for Test One: 下午 07:25:00
Waiting....    Press Return
Iterating...
下午 07:25:02
下午 07:25:02
下午 07:25:02
Waiting....    Press Return
Iterating...
下午 07:25:04
下午 07:25:04
下午 07:25:04

這個特性就叫做 Lazy Evaluation 關鍵就在變數 var sequence = Generate(3, () => DateTime.Now);,這個變數 sequence 保存的並不是運算過後的值 而是 expression tree

expression tree 有一個特點就是可以進行拼接,例如下面這個例子可以將 sequence1 拼接在 sequence2 變數上,並且拼接時並不會馬上把 sequence1 計算出來而是拼接完後一起運行,也就是說它是把這兩個功能而不是把數據進行拼接。

var sequence1 = Generate(3, () => DateTime.Now);
var sequence2 = from value in sequence1
				select value.ToUniversalTime();

這種特性就很適合用來操作無窮序列,下面這段程式碼就會產生一個 0 到 MaxValue 的值,但實際上我們只需要前十個值, Lazy Evaluation 就使會檢查前十個值不會完整檢查到 MaxValue。

void Main()
{
	var answers = from number in AllNumbers()
				  select number;
	var smallNumbers = answers.Take(10);
	foreach (var num in smallNumbers)
		Console.WriteLine(num);
}

static IEnumerable<int> AllNumbers()
{
	var number = 0;
	while (number < int.MaxValue)
	{
		yield return number++;
	}
}

但如果你寫了 where 子句那麼就會一一檢查所有元素,所以整個集合都會全部檢查一次,會產生這樣的結果的方法還包含 OrderBy, Max, Min, 因為它們同樣也需要所有元素的內容才能產出結果。

void Main()
{
	var answers = from number in AllNumbers()
				  where number < 10
				  select number;
	foreach (var num in answers)
		Console.WriteLine(num);
}

static IEnumerable<int> AllNumbers()
{
	var number = 0;
	while (number < int.MaxValue)
	{
		yield return number++;
	}
}

所以在寫 query expression 的時候要注意子句的執行順序,一個正確的順序可以縮小方法的處理範圍,第一種寫法會先排序所有元素才進行過濾, 第二種寫法比較好先過濾縮小範圍才進行排序。

var sortedProductsSlow =
	   from p in products
	   orderby p.UnitsInStock descending
	   where p.UnitsInStock > 100
	   select p;

var sortedProductsFast =
	   from p in products
	   where p.UnitsInStock > 100
	   orderby p.UnitsInStock descending
	   select p;

Eager EvaluationLazy Evaluation 相反,他會馬上處理你的查詢語句,背後的概念跟快照差不多, 可以利用 ToList()ToArray() 直接保存完整的資料到記憶體內,通常是馬上需要處理數據或者之後的操作需要同一個查詢結果才會用到 Eager Evaluation

把之前用過的範例加上 ToList() 之後 sequence1 就不再是保存 expression tree 而是實際的 List, 所以對 sequence2 來說它兩次操作的 sequence1 其實都是同一份數據結果。

var sequence1 = Generate(3, () => DateTime.Now).ToList();
var sequence2 = from value in sequence1
				select value.ToUniversalTime();

sequence2.Dump();
Console.ReadLine();
sequence2.Dump();

Summary

Lazy Evaluation 可以大大的減少程式的工作量運作起來也比較智能,但是需要 Eager Evaluation 的場合也可以使用 ToList()ToArray() 將數據值保存到記憶體內,除非真的很確定需要 Eager Evaluation 的特性否則優先使用 Lazy Evaluation