【原创+散分】逐步为对象集合构建一个通用的按指定属性排序的方法

orain 2009-04-07 01:59:36
加精
有时候我们需要对集合中的自定义对象进行排序,以最原始的 System.Array 为例,如
Person[] people = new Person[]{
new Person(3, "Andy", new DateTime(1982, 10, 3)),
new Person(1, "Tom", new DateTime(1993, 2, 10)),
new Person(2, "Jerry", new DateTime(1988, 4, 23))
};

类 Person 的定义为:
class Person
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }

public Person(int id, string name, DateTime birthday)
{
Id = id;
Name = name;
Birthday = birthday;
}

public override string ToString()
{
return String.Format("Id: {0,-6}Name: {1,-20}Birthday: {2:yyyy-MM-dd}", Id, Name, Birthday);
}
}

可能会需要根据 Id、Name 及 Birthday 进行排序。在 .NET 中,自定义对象数组排序最常见的实现方式是在对象中实现 IComparable 接口,然后调用 Array.Sort(array) 静态方法,显然,在上述情形下,这不是一个好的解决办法。其实 .NET 提供了一个泛型的 Sort() 静态方法,可以根据指定的谓词函数进行排序,其定义如下:
public static void Sort<T>(
T[] array,
Comparison<T> comparison
)

按照定义,我们先定义一个谓词函数:
static int CompareById(Person first, Person second)
{
if (first.Id > second.Id)
return 1;
if (first.Id == second.Id)
return 0;
return -1;
}

然后在排序时,如下调用:
Array.Sort(people, new Comparison<Person>(CompareById));

使用语句输出结果:
foreach (Person p in people)
Console.WriteLine(p);

可以看到 Person 数组已经按照 Id 排序了。因为 .NET 内置的类型大多都实现了 IComparable 接口,包括值类型,所以上面的谓词函数可以简化为:
static int CompareById(Person first, Person second)
{
return first.Id.CompareTo(second.Id);
}

虽然这个函数写起来很简单,但是对每个需要进行排序的属性都写一个函数,也挺麻烦,幸好 .NET 2.0 提供了匿名委托,不用再单独定义函数了:
Array.Sort(people, delegate(Person first, Person second){
return first.Id.CompareTo(second.Id);
});

简单了许多,如果是 .NET 3.5,可以用 Lamda 表达式进一步简化:
Array.Sort(people, (first, second) => first.Id.CompareTo(second.Id));

在实际应用开发中,从性能和易用性上来说,到这一步大多数情形下已经足够。下面的部分可能有过度设计的嫌疑,但这里主要是研究 .NET 一些特性的使用,所以我们继续往下。
能否直接返回一个委托,使我们不必关心 Person 类的具体属性比较,而直接根据属性进行排序呢?答案是肯定的。为 Person 类添加一个静态方法:
public static Comparison<Person> CompareByProperty(string name)
{
switch (name)
{
case "Id":
return (first, second) => first.Id.CompareTo(second.Id);
case "Name":
return (first, second) => first.Name.CompareTo(second.Name);
case "Birthday":
return (first, second) => first.Birthday.CompareTo(second.Birthday);
default:
throw new Exception("属性 " + name + " 不存在。");
}
}

排序时,可以这样调用:
Array.Sort(people, Person.CompareByProperty("Birthday"));

还行,但是如果为 Person 类增加了新的属性,如果要按照新属性排序,必须要修改代码,能不能做到增加新属性而不修改代码呢?当然可以。因为要用到反射,为简化代码,突出主题,我们假定所有使用到的属性都实现了 IComparable 接口,修改上面的 CompareByProperty(string) 方法为:
public static Comparison<Person> CompareByProperty(string name)
{
Type typeOfPerson = typeof(Person);
PropertyInfo p = typeOfPerson.GetProperty(name);
if (p == null)
throw new Exception("属性 " + name + " 不存在。");
// 假定该类所有的属性均实现了接口 IComparable
return (first, second) => ((IComparable)p.GetValue(first, null)).CompareTo(p.GetValue(second, null));
}

因为方法的签名仍保持一致,所以调用的语句不用修改。
仔细观察上面的代码,应该可以把它的应用再扩大化,而不仅限于 Person 类,而这显然是泛型的长项。当然,这样的话,不应再把这个方法放在 Person 类中,我们暂时先把它移到主程序中,稍后再为它寻找一个好的归宿,修改后的CompareByProperty 泛型方法代码如下:
public static Comparison<T> CompareByProperty<T>(string name)
{
Type typeOfPerson = typeof(T);
PropertyInfo p = typeOfPerson.GetProperty(name);
if (p == null)
throw new Exception("属性 " + name + " 不存在。");
// 假定该类所有的属性均实现了接口 IComparable
return (first, second) => ((IComparable)p.GetValue(first, null)).CompareTo(p.GetValue(second, null));
}

调用时需要指定泛型参数:
Array.Sort(people, CompareByProperty<Person>("Name"));

到这里通用性已经很不错了,能否再更进一步呢?下面就是这篇文章所要抵达的终点:为 System.Array 类增加一个通用的按元素对象属性排序的方法,.NET 3.5 中新增了扩展方法,可以在不修改原有类代码的前提下为类增加新的实例方法,这正是我们这里所需要的,这需要新增加一个静态类,完整的代码如下:
static class ExtensionArray
{
public static void SortBy(this Array array, string name)
{
Type elementType = array.GetType().GetElementType();
Type bridge = typeof(Bridge<>).MakeGenericType(elementType);
MethodInfo sortMethod = bridge.GetMethod("Sort");
sortMethod.Invoke(null, new object[] { array, name });
}

private static class Bridge<T>
{
private static Comparison<T> CompareByProperty(string name) //不必再是泛型方法
{
Type typeOfPerson = typeof(T);
PropertyInfo p = typeOfPerson.GetProperty(name);
if (p == null)
throw new Exception("属性 " + name + " 不存在。");
// 假定该类所有的属性均实现了接口 IComparable
return (first, second) => ((IComparable)p.GetValue(first, null)).CompareTo(p.GetValue(second, null));
}

public static void Sort(Array array, string name)
{
Array.Sort((T[])array, CompareByProperty(name));
}
}
}

注意,在上面的代码中增加了一个私有的嵌套类 Bridge,这主要是为了便于调用 Array.Sort<T>() 泛型方法,如果没有这个类进行过渡,则必须使用大量的反射方法才能调用 Array.Sort<T> 方法。
现在按属性排序只需这样调用:
people.SortBy("Birthday");

代码很简单,但是我们应当看到,通用性的扩展是以牺牲性能为代价的。尤其是在后期引入反射以后,性能大幅下降,简单测试了一下,Array.Sort(people, (first, second) => first.Id.CompareTo(second.Id)) 与 people.SortBy("Id") 性能相差约为 120 倍。所以在实际应用中,我们应把握好度,适可而止。但是从学习的角度上来说,我觉得在自己能力范围内尽量深入,还是很有价值的。
...全文
937 128 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
128 条回复
切换为时间正序
请发表友善的回复…
发表回复
zx_lxq 2009-04-13
  • 打赏
  • 举报
回复
学习
MicroDeviser 2009-04-10
  • 打赏
  • 举报
回复
好贴~~~~~~~
Roc_Lee 2009-04-10
  • 打赏
  • 举报
回复
学习原创
typeof 2009-04-10
  • 打赏
  • 举报
回复
学习
FlyBee 2009-04-10
  • 打赏
  • 举报
回复
学习学习
jacklee_008 2009-04-10
  • 打赏
  • 举报
回复
SDF
orain 2009-04-10
  • 打赏
  • 举报
回复
[Quote=引用 120 楼 vwxyzh 的回复:]
来一个直接生成il的,排列大数组的效率接近lambda

C# code public static class Extensions
{

public static void SortByPropertyName<T>(this T[] array, string propName)
{
Array.Sort(array, Inner<T>.GetComparison(propName));
}

private static class Inner<T>
{
private static Dictionary<string, Comparison<T>> m_cache = n…
[/Quote]
呵呵,牛人啊!直接写 IL 确实效率很高,就是难度大了点,也没有研究过。其实对 IL 我更关心的一个问题是,能否利用 IL 获取到函数内部局部变量以及全局变量的值,其实我的目的是想做一个类似于 JavaScript 的 Eval,这个比较重要的一步就是如何获取当前上下文的变量值。如
string str = "abcd";
string str2 = Eval("str") as string
因为对 IL 不熟,或许这个问题问得比较幼稚,但是还烦请 vwxyzh 给解答一下。
sunny906 2009-04-09
  • 打赏
  • 举报
回复
路过,学习
顺便帮顶
qqiuzaihui 2009-04-09
  • 打赏
  • 举报
回复
好东西, 收藏了.
陌上花花 2009-04-09
  • 打赏
  • 举报
回复
学习过,谢谢。
stund 2009-04-09
  • 打赏
  • 举报
回复
积累!!!
ykwfly 2009-04-09
  • 打赏
  • 举报
回复



学习了 谢谢
CopperBell 2009-04-09
  • 打赏
  • 举报
回复
up
vwxyzh 2009-04-09
  • 打赏
  • 举报
回复
忘记写了,T仅限是引用类型
值类型的情况要加一些opcode,麻烦。。。
vwxyzh 2009-04-09
  • 打赏
  • 举报
回复
来一个直接生成il的,排列大数组的效率接近lambda
    public static class Extensions
{

public static void SortByPropertyName<T>(this T[] array, string propName)
{
Array.Sort(array, Inner<T>.GetComparison(propName));
}

private static class Inner<T>
{
private static Dictionary<string, Comparison<T>> m_cache = new Dictionary<string, Comparison<T>>();

internal static Comparison<T> GetComparison(string propName)
{
Comparison<T> comparison;
if (!m_cache.TryGetValue(propName, out comparison))
m_cache[propName] = comparison = GetComparisonNoCache(propName);
return comparison;
}

private static Comparison<T> GetComparisonNoCache(string propName)
{
var prop = typeof(T).GetProperty(propName);
var comparerType = typeof(Comparer<>).MakeGenericType(prop.PropertyType);
DynamicMethod dm = new DynamicMethod(string.Empty, typeof(int), new Type[] { typeof(T), typeof(T) }, typeof(T));
var il = dm.GetILGenerator();
il.Emit(OpCodes.Call, comparerType.GetProperty("Default").GetGetMethod());
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, prop.GetGetMethod());
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Callvirt, prop.GetGetMethod());
il.Emit(OpCodes.Callvirt, comparerType.GetMethod("Compare"));
il.Emit(OpCodes.Ret);
return (Comparison<T>)dm.CreateDelegate(typeof(Comparison<T>));
}
}
}

yagebu1983 2009-04-09
  • 打赏
  • 举报
回复
好贴。。。。
up。。。。
FenixeVon 2009-04-09
  • 打赏
  • 举报
回复
学习
tiger_ok1 2009-04-09
  • 打赏
  • 举报
回复
受益..
outou 2009-04-09
  • 打赏
  • 举报
回复
太好了。
Rogues 2009-04-09
  • 打赏
  • 举报
回复
好帖
加载更多回复(104)

111,093

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • AIGC Browser
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧