寻找一种更好的方法来对我的 List<T> 进行排序

Posted

技术标签:

【中文标题】寻找一种更好的方法来对我的 List<T> 进行排序【英文标题】:Looking for a better way to sort my List<T> 【发布时间】:2011-03-09 15:36:01 【问题描述】:

我正在审查我不久前编写的一段代码,我只是讨厌我处理排序的方式 - 我想知道是否有人可以向我展示更好的方法。

我有一个课程Holding,其中包含一些信息。我有另一个类HoldingsList,其中包含一个List&lt;Holding&gt; 成员。我还有一个枚举,PortfoliosheetMapping,它有大约 40 个元素。

看起来像这样:

public class Holding

    public ProductInfo Product get;set; 
    // ... various properties & methods ...


public class ProductInfo

    // .. various properties, methods... 


public class HoldingsList

    public List<Holding> Holdings get;set;
    // ... more code ...


public enum PortfolioSheetMapping

    Unmapped = 0,
    Symbol,
    Quantitiy,
    Price,
    // ... more elements ...

我有一个方法可以调用 List 以根据用户选择的枚举进行排序。该方法使用了一个 mondo switch 语句,它有 40 多个案例(啊!)。

下面的简短 sn-p 说明了代码:

if (frm.SelectedSortColumn.IsBaseColumn)

    switch (frm.SelectedSortColumn.BaseColumn)
    
        case PortfolioSheetMapping.IssueId:
            if (frm.SortAscending)
            
                // here I'm sorting the Holding instance's
                // Product.IssueId property values...
                // this is the pattern I'm using in the switch...
                pf.Holdings = pf.Holdings.OrderBy
                  (c => c.Product.IssueId).ToList();
            
            else
            
                pf.Holdings = pf.Holdings.OrderByDescending
                  (c => c.Product.IssueId).ToList();
            
            break;
        case PortfolioSheetMapping.MarketId:
            if (frm.SortAscending)
            
                pf.Holdings = pf.Holdings.OrderBy
                  (c => c.Product.MarketId).ToList();
            
            else
            
                pf.Holdings = pf.Holdings.OrderByDescending
                  (c => c.Product.MarketId).ToList();
            
            break;
        case PortfolioSheetMapping.Symbol:
            if (frm.SortAscending)
            
                pf.Holdings = pf.Holdings.OrderBy
                  (c => c.Symbol).ToList();
            
            else
            
                pf.Holdings = pf.Holdings.OrderByDescending
                  (c => c.Symbol).ToList();
            
            break;
        // ... more code ....

我的问题在于 switch 语句。 switchPortfolioSheetMapping 枚举紧密绑定,后者可以在明天或后天更改。每次它发生变化时,我都将不得不重新访问这个 switch 语句,并在其中添加另一个 case 块。我只是担心这个 switch 语句最终会变得如此之大,以至于完全无法管理。

谁能告诉我是否有更好的方法来对我的列表进行排序?

【问题讨论】:

为什么要进行这种排序?是否仅用于展示目的? @Hans,该类实例是从包含投资组合分析数据的 Excel 电子表格反序列化的。实际操作是从 Excel 工具栏按钮调用的(但这实际上无关紧要),一旦完成排序,我会将对象重新序列化回电子表格。排序实际上会影响 Excel 电子表格中的各种其他元素,因此它不是纯粹的仅显示。 扩展我对马克的回答的评论......您是否可以重构代码以更类似于数据库的形式处理数据?如果您使用更多数据库样式的格式,您甚至可能不需要枚举(因为您可以根据需要简单地从“表”中添加和删除列),所以如果您愿意投入时间/精力去做这样的重构,你可能会得到更优雅的东西,可以这么说。当然,您的程序在其他地方的某些方面可能会使这种方法不像看起来那样可行...... @code4life:这与您的问题无关,但如果您使用的是 Excel 电子表格,为什么需要从其中提取数据?是不是不能直接用 COM 来操作数据? @JAB,数据来自投资组合分析电子表格 (Excel),因此在框架的这一部分中,大多数类都旨在支持电子表格功能。要改变这一点需要进行重大的重构......但我觉得这并不相关,所以我一开始就省略了这一点。 【参考方案1】:

您将排序后的数据直接重新分配回您的pf.Holdings 属性,那么为什么不绕过OrderByToList 的开销而直接使用列表的Sort 方法呢?

您可以使用映射为所有支持的排序保存Comparison&lt;T&gt; 代表,然后使用适当的代表调用Sort(Comparison&lt;T&gt;)

if (frm.SelectedSortColumn.IsBaseColumn)

    Comparison<Holding> comparison;
    if (!_map.TryGetValue(frm.SelectedSortColumn.BaseColumn, out comparison))
        throw new InvalidOperationException("Can't sort on BaseColumn");

    if (frm.SortAscending)
        pf.Holdings.Sort(comparison);
    else
        pf.Holdings.Sort((x, y) => comparison(y, x));


// ...

private static readonly Dictionary<PortfolioSheetMapping, Comparison<Holding>>
    _map = new Dictionary<PortfolioSheetMapping, Comparison<Holding>>
    
         PortfolioSheetMapping.IssueId,  GetComp(x => x.Product.IssueId) ,
         PortfolioSheetMapping.MarketId, GetComp(x => x.Product.MarketId) ,
         PortfolioSheetMapping.Symbol,   GetComp(x => x.Symbol) ,
        // ...
    ;

private static Comparison<Holding> GetComp<T>(Func<Holding, T> selector)

    return (x, y) => Comparer<T>.Default.Compare(selector(x), selector(y));

【讨论】:

好建议,绝对值得一试。 谢谢!有趣的是,.Sort 比 .OrderBy().ToList() 略快但绝对快。 很好,我想知道如何强输入我的解决方案 - 这个更好:将其封装在委托中。【参考方案2】:

您可以尝试将开关减少到这样的程度:

    private static readonly Dictionary<PortfolioSheetMapping, Func<Holding, object>> sortingOperations = new Dictionary<PortfolioSheetMapping, Func<Holding, object>>
    
        PortfolioSheetMapping.Symbol, h => h.Symbol,
        PortfolioSheetMapping.Quantitiy, h => h.Quantitiy,
        // more....
    ;

    public static List<Holding> SortHoldings(this List<Holding> holdings, SortOrder sortOrder, PortfolioSheetMapping sortField)
    
        if (sortOrder == SortOrder.Decreasing)
        
            return holdings.OrderByDescending(sortingOperations[sortField]).ToList();
        
        else
        
            return holdings.OrderBy(sortingOperations[sortField]).ToList();                
        
    

您可以使用反射填充排序操作,或手动维护它。如果您不介意稍后在调用者中调用 ToList,您还可以让 SortHoldings 接受并返回 IEnumerable 并删除 ToList 调用。我不能 100% 确定 OrderBy 是否乐意收到物品,但值得一试。

编辑:请参阅 LukeH 的解决方案以保持强类型化。

【讨论】:

我正要推荐一本比较类的字典,但这更整洁! 感谢您的提示!这绝对更容易编码,但我想我会接受@LukeH 的建议,改为使用 .Sort() ,因为它更快一些。但在概念上与您的方法相同(使用字典等)。【参考方案3】:

你看过Dynamic LINQ

具体来说,您可以简单地执行以下操作:

var column = PortFolioSheetMapping.MarketId.ToString();
if (frm.SelectedSortColumn.IsBaseColumn)

    if (frm.SortAscending)
         pf.Holdings = pf.Holdings.OrderBy(column).ToList();
    else
         pf.Holdings = pf.Holdings.OrderByDescending(column).ToList();

注意:这确实有你的枚举匹配你的列名的约束,如果这适合你的话。

编辑

第一次错过了Product 属性。在这些情况下,DynamicLINQ 将需要查看,例如,"Product.ProductId"。您可以反映属性名称或简单地使用“知名”值并与枚举.ToString() 连接。在这一点上,我只是在强迫我回答你的问题,以便它至少是一个可行的解决方案。

【讨论】:

我认为这是一个糟糕的答案,它看起来像“阅读这个,可能会有所帮助,我懒得帮你找到解决方案”=) @Marc,感谢您的链接(不是双关语),但这将如何帮助减少我巨大的 switch 语句? 哇,我去编辑我的帖子以添加一些特定的内容,然后就会出现发痒的触发器。老天保佑我是打字慢的人! ;) @Restuta: code4life 正在处理的数据似乎可以在数据库表示中最有效地处理(对我来说突出的部分是数据具有一组命名列的事实,每个都可以是数据的排序列)。 Marc 发布了一个链接,解释了如何动态进行此类查询,这正是 code4life 想要的。 @JAB 没有任何解释,他的答案没有用,这可以通过他编辑答案的事实来证明。【参考方案4】:

怎么样:

Func<Holding, object> sortBy;

switch (frm.SelectedSortColumn.BaseColumn)

    case PortfolioSheetMapping.IssueId:
        sortBy = c => c.Product.IssueId;
        break;
    case PortfolioSheetMapping.MarketId:
        sortBy = c => c.Product.MarketId;
        break;
    /// etc.


/// EDIT: can't use var here or it'll try to use IQueryable<> which doesn't Reverse() properly
IEnumerable<Holding> sorted = pf.Holdings.OrderBy(sortBy);
if (!frm.SortAscending)

    sorted = sorted.Reverse();

?

不完全是最快的解决方案,但它相当优雅,这正是您所要求的!

编辑: 哦,对于 case 语句,它可能需要重构为一个返回 Func 的单独函数,这并不是完全摆脱它的好方法,但你至少可以将它隐藏在你的过程中间!

【讨论】:

@Michael 修复了我复制错误的原件,如果现在正确,是吗? 没错,但我更喜欢 IEnumerable sorted = (frm.SortAscending) ? pf.Holdings.OrderBy(sortBy):pf.Holdings.OrderByDescending(sortBy)。这避免了 Reverse() @Michael 我不确定它是否有很大的不同,从 Reflector 的输出来看,OrderByDescending 似乎只是在内部返回了一个反转的枚举器,但我明白你的意思。【参考方案5】:

在我看来,我们可以立即做出两个改进:

使用frm.SortAscendingOrderByOrderByDesccending之间做出决定的逻辑在每个case中都有重复,如果cases被更改,可以拉到switch之后除了建立排序键并将其放入Func

当然仍然留下switch本身-这可以用静态映射(例如Dictionary)从PortfolioSheetMappingFunc替换Holding和返回排序键。例如

【讨论】:

谢谢,这些都是很好的建议,绝对有助于更好地模块化代码。【参考方案6】:

您可以实现一个使用反射的自定义 IComparer 类。不过这样会慢一些。

这是一个我曾经用过的类:

class ListComparer : IComparer

    private ComparerState State = ComparerState.Init;
    public string Field get;set;


    public int Compare(object x, object y) 
    
        object cx;
        object cy;

        if (State == ComparerState.Init) 
        
            if (x.GetType().GetProperty(pField) == null)
                State = ComparerState.Field;
            else
                State = ComparerState.Property;
        

        if (State == ComparerState.Property) 
        
            cx = x.GetType().GetProperty(Field).GetValue(x,null);
            cy = y.GetType().GetProperty(Field).GetValue(y,null);
        
        else 
        
            cx = x.GetType().GetField(Field).GetValue(x);
            cy = y.GetType().GetField(Field).GetValue(y);
        


        if (cx == null) 
            if (cy == null)
                return 0;
            else 
                return -1;
        else if (cy == null)
            return 1;

        return ((IComparable) cx).CompareTo((IComparable) cy);

    

    private enum ComparerState 
    
        Init,
        Field,
        Property
    

然后像这样使用它:

var comparer = new ListComparer()  
    Field= frm.SelectedSortColumn.BaseColumn.ToString() ;
if (frm.SortAscending)
    pf.Holding = pf.Holding.OrderBy(h=>h.Product, comparer).ToList();
else
    pf.Holding = pf.Holding.OrderByDescending(h=>h.Product, comparer).ToList();

【讨论】:

我愿意看看这个——你能详细说明一下如何使用反射吗?不知道这如何适合我的 switch 语句和枚举值... 但是,如果您的框架版本允许,我更喜欢动态 LINQ 解决方案!【参考方案7】:

如果Holding 类中的属性(符号、价格等)是同一类型,您可以执行以下操作:

var holdingList = new List<Holding>()

      new Holding()  Quantity = 2, Price = 5 ,
      new Holding()  Quantity = 7, Price = 2 ,
      new Holding()  Quantity = 1, Price = 3 
;

var lookup = new Dictionary<PortfolioSheetMapping, Func<Holding, int>>()

       PortfolioSheetMapping.Price, new Func<Holding, int>(x => x.Price) ,
       PortfolioSheetMapping.Symbol, new Func<Holding, int>(x => x.Symbol) ,
       PortfolioSheetMapping.Quantitiy, new Func<Holding, int>(x => x.Quantity) 
;

Console.WriteLine("Original values:");
foreach (var sortedItem in holdingList)

    Console.WriteLine("Quantity = 0, price = 1", sortedItem.Quantity, sortedItem.Price);


var item = PortfolioSheetMapping.Price;
Func<Holding, int> action;
if (lookup.TryGetValue(item, out action))

    Console.WriteLine("Values sorted by 0:", item);
    foreach (var sortedItem in holdingList.OrderBy(action))
    
         Console.WriteLine("Quantity = 0, price = 1", sortedItem.Quantity, sortedItem.Price);
    

然后显示:

Original values:Quantity = 2, price = 5Quantity = 7, price = 2Quantity = 1, price = 3

Values sorted by Price:Quantity = 7, price = 2Quantity = 1, price = 3Quantity = 2, price = 5

【讨论】:

我喜欢 Func 的想法。 Holding 类中的属性是不同的类型,因此我必须根据您的建议采取修改方法,但仍然是个好主意。 我刚刚看到我在做我的时候发布的其他答案,您可以将查找更改为 lookup = new Dictionary> 这应该可以工作

以上是关于寻找一种更好的方法来对我的 List<T> 进行排序的主要内容,如果未能解决你的问题,请参考以下文章

哪个更好?数组、ArrayList 或 List<T>(在性能和速度方面)

我正在寻找一种更短/更好的方法来从我的核心数据应用程序中的 NSSet 获取第一个对象

有没有比计时更好的方法来对 C 程序进行基准测试?

List<T> vs BindingList<T> 优点/缺点

从 List<T> 到数组 T[] 的转换

Java 8字符串比较