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 类中应对这个问题?