65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义排序任意顺序的对象列表

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (14投票s)

2010年11月2日

CPOL

4分钟阅读

viewsIcon

81966

downloadIcon

92

一个IList扩展方法,可以按照任意顺序或组合顺序对列表进行排序。

引言

最近,我在工作中遇到了一个问题 - 一份账户对象列表需要在报告中以特定的顺序显示,顺序要求如下:

  • ISA Stock 
  • ISA Cash 
  • Wrap Cash 
  • Personal Portfolio 

这个顺序是根据Account对象的AccountType属性来确定的。显然,我不能使用IEnumerable.OrderBy()方法,因为要求的顺序不是字母顺序。我可以给Account对象添加一个额外的属性 - Order - 这样ISA Stock账户将拥有Order = 1;ISA Cash账户拥有Order = 2,依此类推,然后按Order属性对账户进行排序。但我不喜欢为了排序而增加类负担的想法。此外,在不同的场景下,顺序可能会不同。

另一个可能的解决方案是让Account类实现IComparable接口,但同样,不同场景下的顺序可能会不同,并且实现IComparable只能处理一种特定的顺序。此外,有时你可能无法修改要排序的类。所以一个更好的解决方案是将排序算法与要排序的对象分离开来。这样,它们就可以独立变化 - 这是GoF设计模式书中的一个老套路:)

因此,我最终开发了一个IList扩展方法和一个Sorter类,它模仿了IEnumerable.OrderBy()的语法,并允许你自定义排序(而不是字母顺序)对象列表,根据其字符串或枚举属性进行排序。(与普遍看法相反,我的Sorter类并没有实现IComparer接口,原因我稍后会讨论。)

下载源代码  

感谢Paw Jershauge指出源代码下载位置

https://codeproject.org.cn/KB/106570/Collections/CustomSort.zip   

如何使用CustomSort 

按单个属性对列表进行排序: 

_listToBeSorted.Sort(x => x.AccountType, "ISA Stock,ISA Cash,Wrap Cash,Personal Portfolio");

你可以指定一个StringComparison来控制在排序时如何比较字符串

_listToBeSorted.Sort(x => x.AccountType, "ISA Stock,ISA Cash,Wrap Cash,Personal Portfolio", 
                                          StringComparison.InvariantCultureIgnoreCase);

如果你想按多个属性排序,你必须使用Sorter类。我采用了**Fluent Interface**技术,使代码更具可读性

var sorter = new Sorter<ClassToBeSorted, string>(); 

_listToBeSorted.Sort(
    sorter.Add(x => x.AccountType, "ISA Stock,ISA Cash,Wrap Cash,Personal Portfolio") 
          .Add(x => x.AccountName, "Peter,Adam", StringComparison.InvariantCultureIgnoreCase)
          .Add(x => x.AccountLocation, "London,Edinburgh") 
); 

如果你的对象包含其他对象作为属性,例如

public class CompositeClassToBeSorted { 
   public ClassToBeSorted ClassProperty; 
   public string ClassName; 
}

你可以深入任意层级,使用某个属性进行排序

_listToBeSorted.Sort(x => x.ClassProperty.AccountType, "Wrap,Cash");

IEnumerable.OrderBy()方法具有相同的功能,但当要排序的对象属性是其他对象的集合,并且你想按集合中的某个元素排序时,它不会正确排序。例如,如果你有一个类像这样

public class ListClass
{
    public List<ClassToBeSorted> ClassProperty;
}

以下代码将无效

_listToBeSorted.OrderBy(x => x.ClassProperty[3].AccountType);

但是,你可以使用我的自定义排序器来按List类型属性中的特定元素进行排序

classList.Sort(x => x.ClassProperty[0].AccountType, "Wrap,Cash");

排序行为、实现和性能 

如果你的整个列表包含 A、B、C、D、E、F,而你只指定了部分顺序,即

_listToBeSorted.OrderBy(x => x.AccountType,"B,E"); 

那么 B、E 将排在列表的前面,其余元素将保持在原来的位置。

最初,我让CustomSort实现了IComparer接口,并使用IList.Sort(IComparer)方法来排序列表。如果自定义排序顺序包含了列表中的所有元素,那么它工作得很好。但当我们只指定部分顺序时,剩余的元素不会停留在原地,而是会有点随机化(或者,可能有一个我没有看到的模式)。所以最终我选择自己手动排序列表。

性能可以更好。我用1000个对象的列表进行了测试。如果只按一个属性排序,排序会在瞬间完成。但如果你按两个或三个属性排序,排序会花费几秒钟。我在附带的源代码文件中包含了一个性能测试。你可以自己判断性能是否令你满意。

可能的改进

  1. 目前,自定义排序只适用于通用的IList。对IEnumerable做同样的事情也很容易。只是我觉得IList是最常见的集合,并且你可以通过调用ToList()方法轻松地将IEnumerable转换为List
  2. 自定义排序可以按List类型的属性进行排序,例如classList.Sort(x => x.ClassProperty[0].AccountType, "Wrap,Cash"),但对于Dictionary类型的属性则不行。同样,这也可以轻松实现。
  3. 性能可以改进,但我还没有费心去尝试各种排序算法。我只是做了足够的工作来满足我自己的工作情况的要求。

关于源代码和单元测试 

源代码是用.NET 3.5编写的。一些.NET 4.0的功能,如可选参数、动态等,会让代码更简洁,甚至可能更高效,但为了更好的向后兼容性,我暂时保留了.NET 3.5。项目文件应该能在VS2010中正常打开和转换(我测试过,并且可行)。我将尽快提供.Net 4.0版本的源代码副本。

测试是用xunit以**行为驱动开发**(BDD)的风格编写的。当在ReSharper中执行时,它会显示类似文档的精美规范。要运行测试,你的ReSharper需要安装xunit测试运行器插件。

 

© . All rights reserved.