For,do… while,while ,foreach是大多數程式設計語言中常用的迴圈控制語句,在C#中查詢運算式也能實現同樣的效果。
查詢運算式使得編程風格從”命令式”變得更加的”聲明式”。查詢運算式定義想要的結果以及要達到該結果需要執行的方法,而不是具體的如何?。這裡重點是查詢運算式,通過擴充方法,查詢運算式使得能夠比命令式迴圈構造更能夠清楚的表達意圖。
下面的語句顯示了一個命令式風格的填充一個數組並列印到控制台上:
int[] foo = new int[100];for (int num = 0; num < foo.Length; num++){ foo[num] = num * num;}foreach (int i in foo){ Console.WriteLine(i.ToString());}
即使實現這麼一小點功能,依然注重的是如何?而不是怎樣實現,將這段代碼改為查詢運算式能夠使得代碼更易讀和使用:
int[] foo = (from n in Enumerable.Range(0, 100) select n * n).ToArray();
第二個迴圈可以利用擴充方法改寫為:
foo.ForAll(n => Console.WriteLine(n));
或者更為簡潔的:
foo.ForAll(Console.WriteLine);
在.NET BCL中ForEach擴充方法有針對List<T>的實現方法。我們可以添加一個針對IEnumerable<T>的ForAll擴充方法實現如下:
public static class Extensions{ public static void ForAll<T>(this IEnumerable<T> sequence, Action<T> action) { foreach (T item in sequence) { action(item); } }}
可能看起來不起眼,只不過是一個擴充方法而已,但是這能夠使得代碼能夠更多的重用,任何時候要對一個序列元素執行操作,ForAll就能夠派上用場。
以上問題可能看上去比較簡單,不足以看出使用查詢文法所帶來的好處,下面來看個複雜一點的。
很多操作需要嵌套的迴圈操作,假設需要產生一個從0到99的(X,Y)對,通常的做法是:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices(){ for (int x = 0; x < 100; x++) { for (int y = 0; y < 100; y++) { yield return Tuple.Create(x, y); } }}
或者改為查詢運算式:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices(){ return from x in Enumerable.Range(1, 100) from y in Enumerable.Range(1, 100) select Tuple.Create(x, y);}
兩者看起來差不多,但是隨著問題的複雜,查詢運算式仍然能夠保持簡潔。現在將問題改為:只產生x和y相加小於100的點對,比較兩者的實現:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices2(){ for (int x = 0; x < 100; x++) { for (int y = 0; y < 100; y++) { if (x+y<100) yield return Tuple.Create(x, y); } }}public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices2(){ return from x in Enumerable.Range(1, 100) from y in Enumerable.Range(1, 100) where x+y<100 select Tuple.Create(x, y);}
差距仍不明顯,但是命令式語句開始隱藏我們想要的意圖。再將問題變得複雜一些。現在,我們需要將返回的點按照距離原點位置的距離降序排列。
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices3(){ var storage = new List<Tuple<int, int>>(); for (int x = 0; x < 100; x++) { for (int y = 0; y < 100; y++) { if (x+y<100) storage.Add(Tuple.Create(x, y)); } } storage.Sort((point1,point2)=> (point2.Item1*point2.Item1+point2.Item2*point2.Item2).CompareTo (point1.Item1 * point1.Item1 + point1.Item2 * point1.Item2) ); return storage;}public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices3(){ return from x in Enumerable.Range(1, 100) from y in Enumerable.Range(1, 100) where x + y < 100 orderby (x * x + y * y) descending select Tuple.Create(x, y);}
現在,差距開始出現了,命令式的風格時的代碼難以理解,如果不認真的看,很難一下子明白比較函數後面一堆東西返回的是什麼。如果沒有注釋的話,這段命令式代碼比較難懂。命令式風格的代碼過多的強調了代碼執行的過程,很容易在這複雜的過程中迷失我們最初想要達到的目的。
查詢運算式比迴圈控制結構更好的一點是:查詢表示式能夠更好的組合,他能夠將演算法組織在一個很小的精簡的片段內來執行一系列的操作。查詢的惰性執行模型也使得開發人員能夠在一個迴圈類執行一系列的操作。而查詢運算式則不能做到這一點。你必須儲存每一次迴圈的結果,然後構造新的迴圈操作來執行上一次的操作結果。
最後一個例子示範了如何工作。操作組合了過濾(where子句),排序(orderby 子句)以及投影(select語句),所有這些操作在一次遍曆操作中完成。而命令式版本的方法需要建立一個零時的儲存物件storage,然後對這個對象分別執行一些列操作。
每一個查詢運算式有一個對應的方法調用是的運算式,有時候,查詢運算式更自然有時候方法調用是的運算式更自然。顯然在上面的例子中,查詢運算式版本更易讀,下面是方法調用的版本:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices3(){ return Enumerable.Range(1, 100). SelectMany(x => Enumerable.Range(1, 100), (x, y) => Tuple.Create(x, y)).Where(pt => pt.Item1 + pt.Item2 < 100). OrderByDescending(pt => pt.Item1 * pt.Item1 + pt.Item2 * pt.Item2);}
在這個例子中,查詢運算式可能比方法運算式更易讀,但其他例子可能不同。有些方法運算式並沒有對應的查詢運算式。一些方法,如Take,TakeWhile,Skip,SkipWhile,Min,以及Max在一些情況下需要使用方法運算式。
有些人可能會人會查詢運算式在執行速度上比迴圈執慢,這一點要看具體的情況。有時候清晰的代碼可能比速度更重要。在改進程式演算法之前,可以考慮LINQ的並行擴充庫PLINQ,可以使用.AsParallel()來使的查詢操作能夠並存執行。
C#最開始是一種命令式程式設計語言,你能夠使用之前最熟悉的方法去編寫代碼。但是這些傳統的方法可能不是最好的。當你發現需要使用迴圈結構去執行某種操作的時候,試試能夠將它改寫為查詢運算式的方式,如果查詢運算式不起作用,試試方法運算式,在大多數情況下,你會發現使用查詢運算式或者方法運算式會比使用傳統的迴圈式的命令式結構能使得代碼變得更加簡潔和清晰。