首页 > 代码库 > 泛型集合的静态扩展方法
泛型集合的静态扩展方法
C# 中的泛型集合提供了很多基于 Enumerable 的静态扩展方法,例如 Find, OrderBy , Average ,Distinct 等,在使用基础数据类型的集合时,可以直接调用这些方法,但如果是自定义类型就应当根据扩展方法所要求的接口,实现自定类型的扩展接口类,其实质就是使用扩展类的实例方法完成特定的操作逻辑,否则对集合的操作将是无效的。本文以 OrderBy 为例进行说明 。
首先需要一个自定义的类 ,这是一个表示自然人属性的简单类,列出了姓名、年龄和性别(这里使用了枚举类型),方便之后按照某一种属性进行排序。
public class Person { public Person(string name, int age, Sex sex) { Name = name; Age = age; Sex = sex; } public String Name { get; set; } public int Age { get; set; } public Sex Sex { get; set; } public override string ToString() { return string.Format(string.Format("Name : {0} \t Age :{1} \t Sex :{2}", Name, Age, Sex.SexString())); } }
下一步是创建一个集合,加入一些随意的集合元素。
List<Person> list = new List<Person>(); list.Add(new Person("MISS DD", 20, Sex.Female)); list.Add(new Person("MR. DD", 19, Sex.Male)); list.Add(new Person("MRS DD", 23, Sex.Unknow)); // 用以保存排序结果 List<Person> result = new List<Person>();
首先试一下基本的数据类型,按照年龄执行排序 :
只要一句 list.OrderBy(p => p.Sex) ,不需要任何多余的代码就可以完成 。具体是怎么实现的呢?
当然先看文档定义了, OrderBy 静态扩展方法在 public static class Enumerable 类中定义 :
// // 摘要: // 根据键按升序对序列的元素排序。 // // 参数: // source: // 一个要排序的值序列。 // // keySelector: // 用于从元素中提取键的函数。 // // 类型参数: // TSource: // source 中的元素的类型。 // // TKey: // keySelector 返回的键的类型。 // // 返回结果: // 一个 System.Linq.IOrderedEnumerable<TElement>,其元素按键排序。 // // 异常: // System.ArgumentNullException: // source 或 keySelector 为 null。 public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);
定义一目了然,这个方法列表中列出了俩个参数。第一个用 this 前缀,说明本方法扩展的类型,第二个参数是一个函数委托,相当于C++中的函数指针,就是代码在此处将要带入一个函数的意思。其中 TSource 和 TKey 表示函数的类型要求。
在 list.OrderBy(p => p.Sex) ; 中没有看到这个表示类型参数是因为 List 集合已经实现 IEnumerable 接口,可以省略这个参数,匿名表达式可以自己推断出类型。
当然也可以使用强类型的语法写成这样:
list.OrderBy<Person, int>(delegate(Person p) { return p.Age; });
使用上面的这个种写法更容易看清楚 TSource 和 TKey 的用意。在匿名委托中带入参数 的类型 为 TSource (Person),返回值的类型为 TKey (int)。说成大白话就是 TSource 就是 Person ,需要排序的List中的元素类型,TKey 就是 int ,我们的 Age 属性的类型,也就是我门选择作为排序标准的关键字。
那如果我们想实现自己的排序,比如现在是去排队,需要女士优先的原则,那我们的是不是可以直接把 TKey 换成 Sex不就行了么?
list.OrderBy<Person, Sex>(delegate(Person p) { return p.Sex; });
试试看吧:
执行后发现顺序已经发生改变了,可是 女士优先的原则没有被实现。但现在的排序是按照什么执行的呢?
在 C# 中枚举类型,严格地说不能是算是基础数据类型,但是其实现是在整型数值的基础上实现的,可以定义为 enum EnumClass : short (或者 int /Long) 这样的整数值类型,继续观察排序结果好像是跟枚举成员的顺序一致。
想到这一点上就可以猜出来枚举集合的排序可能是按照枚举成员所对应的整数值进行的。现在可以验证一下,修改枚举元素的默认值
public enum Sex : ushort { Unknow =3 , Male = 1, Female = 2, }
再次执行,结果如下 :
可以看到排列的顺序已经刷新了,定义值最小的 Male 排在了第一位,值最大的 Unknow 排在了最后 。那么枚举集合默认按照成员定义值大小排序 的猜测已经的到验证。
继续回到我们的问题 : 实现女士优先的原则,那么根据上面的结论只需要将 Famle 的值设为最小就可以完成。这应该是最简单清晰的办法了。
不过现实往往是残酷的,光想怎么简单怎么办不一定靠谱呀!比如,如果人员的信息数据是从数据库中读取的,而且性别的定义已经在数据库中固定下来不能修改了,这时候怎么办?或者我们的类型已经不是简单的枚举,而是更加复杂的类呢?
办法 .Net 已经为我们提供好了,在 MSDN 的文档中 OrderBy 还有另外的一种定义,这种定义允许我们实现自己任何目的的自定义排序 -----(很庆幸之前学过点于C++的模板,这里看的这样的定义相当的亲切)
// // 摘要: // 使用指定的比较器按降序对序列的元素排序。 // // 参数: // source: // 一个要排序的值序列。 // // keySelector: // 用于从元素中提取键的函数。 // // comparer: // 一个用于比较键的 System.Collections.Generic.IComparer<T>。 // // 类型参数: // TSource: // source 中的元素的类型。 // // TKey: // keySelector 返回的键的类型。 // // 返回结果: // 一个 System.Linq.IOrderedEnumerable<TElement>,将根据某个键按降序对其元素进行排序。 // // 异常: // System.ArgumentNullException: // source 或 keySelector 为 null。 public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer);
在这种定义中,多了 一个 接口类型的参数,该接口类需要实现 IComparer 比较器。从 TKey 类型的说明中,可以看出这个接口是 Sex 属性所需要实现的。
那么就开始干吧,对号入座需要创建一个继承 IComparer<Sex> 的接口类,对于 IComparer 接口也很简单,它的返回值表示第一个参数相对于第二个参数的大小标志。如果小就返回小于0的值,大就返回大于0的值,相等返回0 。几乎所有的集合操作都离不开它,那么对于我们的场景,需要在比较时加入女生优先(就是让女生返回小于0的值)的原则。
/// <summary> /// 实现比较器的接口类 /// </summary> public class ISexComparer : IComparer<Sex> { public int Compare(Sex x, Sex y) { if (x == Sex.Female) return -1; else if (y == Sex.Female) return 1; return (int)x - (int)y; } }
现在接口类有了,我们怎么调用的呢?
其实函数签名的参数很容易看懂,但是怎么写代码的时候就不一定对不上了,不怕大家笑话,俺第一次用这种样式的方法的时候一眼就看出来需要这个一个比较器类了,顿时觉得很自豪,自己简直太聪明了,然后,然后带入参数,然后就很天真地这么写了
result = list.OrderBy(p => p.Sex, ISexComparer);
就是把这个类型写了进去,再一看不对啊,怎么报错,换成 typeof(ISexComparer) ,我去还报错,没救了。 当时愣是犯傻不知道该怎么写了,活活憋了好一会才服了软去问了度娘。。。现在想想就笑死了,为毛不认真看一下参数的要求呢?不是某种类型,是 变量啊!变量啊!
// 创建比较器 ISexComparer sexComparer = new ISexComparer(); // 将比较器作为参数带入 list.OrderBy<Person, Sex>(delegate(Person p) { return p.Sex; }, sexComparer);
完成,当然图省事可以合并成一行代码 :
list.OrderBy(p => p.Sex, new ISexComparer());
再次 review 下结果吧:
大功告成,和预期的输出结果一样。
最后,C#的集合提供了很多好用的泛型操作,都是基于静态扩展方法的,其实质就是C++中的模板方法,Fuc<TSource,TKey> 对应函数指针的概念,告诉程序执行时需要调用指定的方法。由于C#中不能直接传递函数指针,所以先约定使用的接口,之后用户可以实现强类型化的接口类,并通过实例变量来传递该函数,最终通过传入函数完成元素移动或选取操作。
最后的最后是俩个小参考:
如果在使用 IComparer 接口时发现程序死循环或者无响应了,请检查 Compare 方法是不是逻辑上出错了。如果我们的上面的 Compare 写成了下面这样
public int Compare(Sex x, Sex y) { if (x == Sex.Female) return -1; else if (x == Sex.Male) return 1; return (int)x - (int)y; }
将会使程序陷入死循环中。猛一看 是 Female 返回-1,Male 返回 1,没错啊?可是这种过滤将参数 y 抛开了,需要记住的是Compare 需要对前后比较的俩个值进行判断,对于同一个条件 X ,Y 都必须作出判断,抛开其中一个,会导致这个位置上的元素一直未被操作,而无法完成任务。
可以使用静态扩展方法替代完成接口的功能或者继承中不方便重写的基类方法。比如Person类中的Sex属性是枚举类型,并没有ToString方法,为了实现方便地输出就使用扩展方法来完成了。
/// <summary> /// 提供方便获取字符串表示的静态扩展方法 /// </summary> public static class SexExt { public static string SexString(this Sex sex) { if (sex == Sex.Female) return "女"; if (sex == Sex.Male) return "男"; return "未知"; } }
泛型集合的静态扩展方法