# 原則8:優先考慮查詢語法(query syntax)而不是循環結構
**By D.S.Qiu**
尊重他人的勞動,**支持原創,轉載請注明[出處](/blog/1982137):[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
C# 語言對不同控制結構都不乏支持: for , while , do/while 和 foreach ,所有都是語言的一部分。從過去的計算機語言設計來看,很難不讓人懷疑語言的設計者不是錯過某些驚奇的循環結構。實際上,總是存在一個更好的方法:查詢語法(query syntax)。
查詢語法可以讓你的程序邏輯從必要模式(imperative model)變為聲明模式(declarative model)。查詢語法定義了答案是什么而且決定了怎樣得到這個答案的特殊實現。上面說到的整個點,這里指的是查詢語法,方法調用語法帶來的好處查詢語句一樣有。這點對于查詢語句很重要,而且進一步擴展,方法語法實現查詢表達模式,比 imperative 循環結構更加清晰表達你的意圖。
這段代碼用必要的方法填充數值然后打印內容到控制臺:
```
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.ToString()));
```
.NET BCL 使用 List<T> 實現 ForAll 。這只是簡單地創建 IEnumerable<T> :
```
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)對。很明顯你會使用嵌套循環:
```
private static IEnumerable<Tuple<int, int>> ProduceIndices()
{
for (int x = 0; x < 100; x++)
for (int y = 0; y < 100; y++)
yield return Tuple.Create(x, y);
}
```
當然,使用查詢你可以產生相同的對象:
```
private static IEnumerable<Tuple<int, int>> QueryIndices()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
select Tuple.Create(x, y);
}
```
看起來很相似,但是查詢更加簡單即使這個問題的描述變得更困難起來。改變問題為產生的(x,y)對要求x和y的和小于100。比較這兩個方法:
```
private static IEnumerable<Tuple<int, int>> 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);
}
private static IEnumerable<Tuple<int, int>> QueryIndices2()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
where x + y < 100
select Tuple.Create(x, y);
}
```
仍然很相近,但是 imperative 語法開始使用必須的語法產生結果而隱藏它的意義。所以再次改變問題。現在,條件一個條件:返回的數組必須按照它們到原點的距離排列。
```
private static IEnumerable<Tuple<int, int>> 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;
}
private static IEnumerable<Tuple<int, int>> QueryIndices3()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
where x + y < 100
orderby (x*x + y*y) descending
select Tuple.Create(x, y);
}
```
有些細節很明顯改變了。 imperative 版本更難去理解。如果你看得快,你幾乎不會注意到比較函數的參數被對換了。那樣保證排序的結果是降序的。沒有注釋或其他支持文檔, imperative 代碼很是很難讀懂。
即使你發現 where 的參數被對換了,你會覺得這是錯誤么? imperative 模式過于強調操作怎么樣執行以至于很容易在這些操作中迷失什么操作正在完成的原意。
還有一個理由使用查詢語法而不是循環結構:查詢可以比循環結構創建更多組合的 API 。查詢運費很自然構造算法塊執行序列上的操作。查詢的模式可以讓開發者枚舉序列中組合單一的操作為的組合操作。循環結構不能有類似的組合。你必須臨時存儲每步的結果,或創建對序列上組合操作的方法。
上個例子就體現了這點。操作組合自 一個過濾操作(where塊),一個排序操作(sort塊)和一個選擇操作select塊)。這些操作全部在一個枚舉操作完成。 imperative 版本創建歷史存儲模型而且排序操作被分離出來。
我幾經討論過查詢語法,但是你應該記住每個查詢操作都有相應的方法調用語法。有時候查詢語法更自然,有時候方法調用語法語法更自然。在上面例子,查詢語法更加可讀。下面是對應的方法調用語法:
```
private static IEnumerable<Tuple<int, int>> MethodIndices3()
{
return Enumerable.Range(0, 100).
SelectMany(x => Enumerable.Range(0,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 , TakeWile , Skip ,SkipWhile , Min ,和 Max 在一定程度上需要你使用方法語法。其他語言,特別是 VB.NET 定義了更多查詢語法的關鍵字。
有的人老是會認為我們討論的這部分即查詢的性能會比循環更慢。雖然你可以用一個簡單例子說明循環能跑贏查詢,但這不是一般地規則。如果你有一個特別的例子即查詢結果表現的不夠好,你才需要決定測量性能。然而,在你完成重寫一個算法之前,考慮使用 LINQ 的并行擴展。使用查詢語法的另一個好處是使用 .AsParallel() 方法可以并行執行查詢。(查看原則35)。
C#開始是一個 imperative 語言。它幾下保留了屬于這個遺產的所有特性。自然你可隨意使用最熟悉的工具(循環結構)。然而,這些工具可能不是最好的工具。當你發現你自己寫著循環結構的形式,問下自己是否使用查詢來寫。如果查詢語法不行,考慮使用函數調用語法。在大多數情況,你會發現創建比使用 imperative 循環結構更清晰的代碼。
小結:
這篇文章講的點還是比較好的,至少是一種新的編程模式(對于像D.S.Qiu這樣的人來說),可以嘗試多使用些,會帶來一些便利。本來一直很糾結 imperative model 和 declararive model 的中文翻譯,由于對計算機語言的歷史都不了解,對一些專業名稱很沒有太多認識,甚至都沒有怎么接觸,只有困惑來了就去看下 wikipedia 。每天都是到這個時候才寫完,脖子今天一直很不舒服,不要有頸椎病呀,完了。
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/1982137](/blog/1982137)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數