自定义排序任意顺序的对象列表
一个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个对象的列表进行了测试。如果只按一个属性排序,排序会在瞬间完成。但如果你按两个或三个属性排序,排序会花费几秒钟。我在附带的源代码文件中包含了一个性能测试。你可以自己判断性能是否令你满意。
可能的改进
- 目前,自定义排序只适用于通用的
IList
。对IEnumerable
做同样的事情也很容易。只是我觉得IList
是最常见的集合,并且你可以通过调用ToList()
方法轻松地将IEnumerable
转换为List
。 - 自定义排序可以按
List
类型的属性进行排序,例如classList.Sort(x => x.ClassProperty[0].AccountType, "Wrap,Cash")
,但对于Dictionary
类型的属性则不行。同样,这也可以轻松实现。 - 性能可以改进,但我还没有费心去尝试各种排序算法。我只是做了足够的工作来满足我自己的工作情况的要求。
关于源代码和单元测试
源代码是用.NET 3.5编写的。一些.NET 4.0的功能,如可选参数、动态等,会让代码更简洁,甚至可能更高效,但为了更好的向后兼容性,我暂时保留了.NET 3.5。项目文件应该能在VS2010中正常打开和转换(我测试过,并且可行)。我将尽快提供.Net 4.0版本的源代码副本。
测试是用xunit以**行为驱动开发**(BDD)的风格编写的。当在ReSharper中执行时,它会显示类似文档的精美规范。要运行测试,你的ReSharper需要安装xunit测试运行器插件。