C# in Depth學習筆記-排序和過濾
1.2 排序和過濾
本節不會改變 Product 型別,我們會使用示例的產品列表,並按名稱排序,然後找出最貴的產品。
每個任務都不難,但我們可以看到它到底能簡化到什麼程度。
1.2.1 按名稱對產品進行排序
以特定順序顯示一個列表的最簡單方式就是先將列表排好序,再遍歷並顯示其中的項。
在.NET 1.1中,這要求使用 ArrayList.Sort ,而且在我們的例子中,要求提供一個 IComparer實現。
也可以讓 Product 型別實現 IComparable ,但那就只能定義一種排序順序。
很容易就會想到,以後除了需要按名稱排序,還可能需要按價格排序。
C# 1使用 IComparer 對 ArrayList 進行排序
程式碼清單1-5實現了 IComparer ,然後對列表進行排序,並顯示它。
//程式碼清單1-5 使用 IComparer 對 ArrayList 進行排序(C# 1) class ProductNameComparer : IComparer { public int Compare(object x, object y) { Product first = (Product)x; Product second = (Product)y; return first.Name.CompareTo(second.Name); } }//測試程式碼 class Program { static void Main() { ArrayList list = Product.GetSampleProducts(); list.Sort(new ProductNameComparer()); foreach (Product p in list) Console.WriteLine(p.Name+":"+p.Price); Console.Read(); } }
在程式碼清單1-5中,要注意的第一件事是,必須引入一個額外的型別來幫助排序。雖然這並不是一個大問題,但假如在一個地方只是想按名稱進行排序,就會感覺編碼工作過於繁重。
其次,注意 Compare 方法中的強制型別轉換。強制型別轉換相當於告訴編譯器我知道的比你多一點點。但是,這也意味著你可能是錯誤的。
如果從 GetSampleProducts 返回的 ArrayList包含一個字串,那麼程式碼會出錯——因為在比較時試圖將字串強制轉型為 Product 。
在給出排序列表的程式碼中也進行了強制型別轉換。這個轉換不如剛才的轉換明顯,因為是編譯器自動進行的。
foreach 迴圈會隱式將列表中的每個元素轉換為 Product 型別。同樣,這種情況在執行時會失敗,在C# 2中,“泛型”可以幫助我們解決這些問題。
C# 2使用 IComparer<Product> 對 List<Product> 進行排序
在程式碼清單1-6中,唯一的改變就是引入了泛型。
//程式碼清單1-6 使用 IComparer<Product> 對 List<Product> 進行排序(C# 2) class ProductNameComparer : IComparer<Product> { public int Compare(Product x, Product y) { return x.Name.CompareTo(y.Name); } } //測試程式碼 List<Product> list = Product.GetSampleProducts(); list.Sort(new ProductNameComparer()); foreach (Product p in list) Console.WriteLine(p.Name+":"+p.Price);
在程式碼清單1-6中,對產品名進行比較的程式碼變得更簡單,因為一開始提供的就是 Product,不需要進行強制型別轉換。
類似地, foreach 迴圈中隱式的型別轉換也被取消了。編譯器仍然會考慮將序列中的源型別轉換為變數的目標型別,但它知道這時兩種型別均為 Product ,因此沒必要產生任何用於轉換的程式碼。
但是我們希望能直接指定要進行的比較,就能開始對產品進行排序,而不需要實現一個介面來做這件事。
C# 2使用 Comparison<Product> 對 List<Product> 進行排序
程式碼清單1-7展示了具體如何做,它告訴 Sort 方法如何用一個委託來比較兩個產品。
//程式碼清單1-7 使用 Comparison<Product> 對 List<Product> 進行排序(C# 2) List<Product> list = Product.GetSampleProducts(); list.Sort(delegate(Product x,Product y) { return x.Name.CompareTo(y.Name); }); foreach (Product p in list) Console.WriteLine(p.Name+":"+p.Price);
注意,現在已經不再需要 ProductNameComparer 型別了。以粗體印刷的語句實際會建立一個委託例項。我們將這個委託提供給 Sort 方法來執行比較。第5章會更多地講解這個特性(匿名方法)。
現在,我們已經修正了在C# 1的版本中不喜歡的所有東西。但是,這並不是說C# 3不能做得更好。首先,將匿名方法替換成一種更簡潔的建立委託例項的方式,如程式碼清單1-8所示。
C# 3在Lambda表示式中使用 Comparison<Product> 進行排序
//程式碼清單1-8 在Lambda表示式中使用 Comparison<Product> 進行排序(C# 3) List<Product> list = Product.GetSampleProducts(); list.Sort((x,y)=>x.Name.CompareTo(y.Name)); foreach (Product p in list) Console.WriteLine(p.Name+":"+p.Price);
Lambda表示式然會像程式碼清單1-7那樣建立一個Comparison<Product> 委託,只是程式碼量減少了。
這裡不必使用 delegate 關鍵字來引入委託,甚至不需要指定引數型別。
除此之外,使用C# 3還有其他好處。可以輕鬆地按順序列印名稱,同時不必修改原始產品列表。
C# 3使用一個擴充套件方法對 List<Product> 進行排序
程式碼清單1-9使用 OrderBy 方法對此進行了演示。
//程式碼清單1-9 使用一個擴充套件方法對 List<Product> 進行排序(C# 3) List<Product> list = Product.GetSampleProducts(); foreach (Product p in list.OrderBy(p=>p.Name)) Console.WriteLine(p.Name+":"+p.Price);
這個方法在 List<Product> 中根本不存在,之所以能呼叫它,是由於存在一個擴充套件方法,第10章將討論擴充套件方法的細節。
這裡實際不再是“原地”對列表進行排序,而只是按特定的順序獲取列表的內容。
有時,你需要更改實際的列表,但有時,沒有任何副作用的排序顯得更“善解人意”。
重點在於現在的寫法更簡潔,可讀性更好(當然是在你理解了語法之後)。
我們的想法是“列表按名稱排序”,現在的程式碼正是這樣做的。並不是“列表通過將一個產品的名稱與另一個產品的名稱進行比較來排序”,就像C# 2程式碼所做的那樣。
也不是使用知道如何將一個產品與另一個產品進行比較的另一種型別的例項來排序。這種簡化的表達方式是C# 3的核心優勢之一。
既然單獨的資料查詢和操作是如此簡單,那麼在執行更大規模的資料處理時,仍然可以保持程式碼的簡潔性和可讀性,這進而鼓勵開發者以一種“以資料為中心”的方式來觀察世界。
本節又展示了一小部分C# 2和C# 3的強大功能,還有許多尚待解釋的語法。
圖1-2展示了C#向更清晰、更簡單的程式碼邁進的這個演變過程。
到目前為止,我們只講了排序.現在來討論一種不同的資料處理方式——查詢。
1.2.2 查詢集合
下一個任務是找出列表中符合特定條件的所有元素。具體地說,要找出價格高於10美元的產品。
在C# 1中,需要執行迴圈,測試每個元素,並打印出符合條件的元素(參見程式碼清單1-10)。
C# 1迴圈、測試和列印
//程式碼清單1-10 迴圈、測試和列印 ArrayList products = Product.GetSampleProducts(); foreach (Product product in products) { if (product.Price > 10m) Console.WriteLine(product); }
上面的程式碼寫起來不難,也很容易理解。然而,請注意3個任務是如何交織在一起的:用 foreach 進行迴圈,用 if 測試條件,再用 Console.WriteLine 顯示產品。
這3個任務的依賴性是一目瞭然的,看看它們是如何巢狀的就明白了。
C# 2測試和列印分開進行
C# 2稍微進行了一下改進(參見程式碼清單1-11)。
//程式碼清單1-11 測試和列印分開進行 List<Product> products = Product.GetSampleProducts(); Predicate<Product> test = delegate (Product p) { return p.Price > 10; }; List<Product> matches = products.FindAll(test); Action<Product> print = Console.WriteLine; matches.ForEach(print);
變數 test 的初始化使用了上節介紹的匿名方法,而 print 變數的初始化使用了C# 2的另一個特性——方法組轉換,它簡化了從現有方法建立委託的過程。
並不是為了證明上述程式碼比C# 1的程式碼簡單,只是說它要強大得多。
具體地說,它使我們可以非常輕鬆地更改測試條件並對每個匹配項採取單獨的操作。
涉及的委託變數( test 和 print )可以傳遞給一個方法——相同的方法可以用於測試完全不同的條件以及執行完全不同的操作。
C# 2測試和列印分開進行的另一個版本
當然,可以將所有測試和列印都放到一條語句中,如程式碼清單1-12所示。
//程式碼清單1-12 測試和列印分開進行的另一個版本 List<Product> products = Product.GetSampleProducts(); Product.FindAll(delegate(Product p){return p.Price>10;}) .ForEach(Console.WriteLine);
這樣更好一些,但 delegate(Product p) 還是很礙事,大括號也是,它們有損可讀性。
C# 3用Lambda表示式來測試
C# 3拿掉了以前將實際的委託邏輯包裹起來的許多無意義的東西,從而有了極大的改進。
//程式碼清單1-13 用Lambda表示式來測試 List<Product> products = Product.GetSampleProducts(); foreach (Product product in products.Where(p => p.Price > 10)) Console.WriteLine(product);
Lambda表示式將測試放在一個非常恰當的位置。再加上一個有意義的方法名,你甚至能大聲念出程式碼,幾乎不用怎麼思考就能理解程式碼的含義。
C# 2的靈活性也得到了保留——傳遞給Where 的引數值可以來源於一個變數。
此外,如果願意,完全可以使用 Action<Product> ,而不是硬編碼的 Console.WriteLine 呼叫。
總結
本節的這個任務強調了我們通過前面的排序任務已經明確的一點——使用匿名方法可以輕鬆編寫一個委託,Lambda表示式則更進一步,將這個任務變得更簡單。
換言之,可以在 foreach迴圈的第一個部分中包含查詢或排序操作,同時不會影響程式碼的可讀性。
圖1-3總結了這些程式設計方式上的變化。對於這個任務來說,C# 4沒有提供任何可以進一步簡化的特性。
現在,我們已經給出了過濾過的列表,接下來假設我們的資料跟以前不一樣了。
如果並非總是知道一個產品的價格,那麼會發生什麼?如何在 Product 類中應對這個問題?