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

7

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

我有一个叫做Holding的类,其中包含一些信息。我还有另一个类HoldingsList,其中包含一个List<Holding>成员。我还有一个枚举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 ...
}

我有一个方法,可以根据用户选择的枚举类型来调用列表进行排序。该方法使用了一个超过40个case的大switch语句(呃!)。

下面是一个简短的代码片段:

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语句上。该switch语句与PortfolioSheetMapping枚举紧密绑定,但该枚举明天或后天可能会更改。每次更改时,我都需要重新查看该switch语句,并添加另一个case块。我只是担心最终这个switch语句会变得如此臃肿而难以管理。
有人能告诉我是否有更好的方法来排序我的列表吗?

为什么要进行这种排序?仅仅是为了显示目的吗? - Hans Passant
@Hans,该类实例是从包含投资组合分析数据的Excel电子表格反序列化而来。实际操作是从Excel工具栏按钮调用的(但这并不重要),一旦排序完成,我会将对象重新序列化回电子表格中。排序实际上会影响Excel电子表格中的各种其他元素,因此它不仅仅是纯显示。 - code4life
2
延伸马克回答的评论,你是否可以重构你的代码,以更类似于数据库的形式处理数据?如果你使用更像数据库的格式(因为你可以根据需要从“表”中简单地添加和删除列),那么你甚至可能不需要枚举,所以如果你愿意花时间和精力进行这样的重构,你最终可能会得到更优雅的东西。当然,在你的程序的其他方面可能存在使这种方法不太可行的方面...... - JAB
@code4life:这并不是与你的问题相关,但如果你正在使用Excel电子表格,为什么需要像那样提取数据?你无法使用COM直接操作数据吗? - JAB
@JAB,数据源自投资组合分析电子表格(Excel),因此在框架的这一部分中,大多数类都旨在支持电子表格功能。要更改它需要进行重构...但我认为提及这一点并不相关,所以一开始我就省略了这个观点。 - code4life
@JAB,我正在使用VSTO。使用COM有点慢,所以我将需要操作的范围提取到一个object[,]数组中进行数据操作。我拥有的类或多或少是数组的门面,然后将其推回到目标工作簿上相应的工作表/范围。 - code4life
7个回答

5
您将排序后的数据直接重新分配给您的pf.Holdings属性,那么为什么不绕过OrderByToList的开销,直接使用列表的Sort方法呢?
您可以使用一个映射来保存所有支持的排序的Comparison<T>委托,然后使用适当的委托调用Sort(Comparison<T>)
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));
}

好的建议,绝对值得一试。 - code4life
传闻中,<code>.Sort</code> 比 .OrderBy().ToList() 稍微快一些,但肯定更快。 - code4life
不错,我一直在想如何为我的解决方案进行强类型化 - 这样做更好:将其封装在委托中。 - Douglas

4
你可以尝试把开关改为以下内容:
    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();                
        }
    }

你可以通过反射来填充sortingOperations,也可以手动维护它。如果你不介意在调用方稍后调用ToList,你也可以使SortHoldings接受并返回IEnumerable,并删除ToList调用。我不确定OrderBy是否能够接收一个对象,但值得一试。
编辑:请参考LukeH的解决方案以保持强类型。

我本来想建议一个比较类的字典,但这个方法更加简洁! - Dr Herbie
谢谢你的建议!这样编码肯定更容易,但我想我会采用@LukeH提出的使用.Sort()的建议,因为它稍微快一些。但从概念上讲,和你一样采用了字典等方法。 - code4life

3

你有没有了解过动态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()连接。此时,我只是强迫我的回答来解决您的问题,以至少得到一个可行的解决方案。


2
我认为这是一个糟糕的答案,看起来像是“阅读这个,也许会有帮助,但我懒得帮你找解决方案”=) - Restuta
@Marc,感谢你提供的链接(无恶意),但这怎么能帮助我减少我的庞大的switch语句呢? - code4life
1
哇,我去编辑我的帖子放一些具体的东西,就开始痒了。天哪,难道我打字慢就不行吗?;) - Marc
@Restuta:code4life正在处理一些看起来最好用数据库表示的数据(让我印象深刻的是数据有一组命名列,每个列都可以成为数据的排序列)。Marc发布了一个链接,解释了如何动态执行这样的查询,这正是code4life想要的。 - JAB
@Marc,我想我表达不清楚。pf.Holdings是一个List<Holding>,而c.Product实际上是引用了一个Product实例。所以简单的OrderBy(string)方法不起作用,据我所知...对此我感到抱歉 - 我会更新我的帖子。 - code4life
1
@JAB没有任何解释,他的答案是无用的,这一事实已被他编辑答案所证明。 - Restuta

1

你可以实现一个使用反射的自定义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语句和枚举值相匹配... - code4life
然而,如果您的框架版本允许,我更喜欢使用动态LINQ解决方案! - Michael Stoll

1

怎么样:

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修复了我复制错误的if语句的原始代码,现在是正确的了,对吗? - Ed James
这是正确的,但我更喜欢IEnumerable<Holding> sorted = (frm.SortAscending) ? pf.Holdings.OrderBy(sortBy) : pf.Holdings.OrderByDescending(sortBy)。这样可以避免使用Reverse()。 - Michael Stoll
@Michael 我不确定这是否有很大的区别,根据Reflector的输出,OrderByDescending似乎只是在内部返回一个反转的枚举器,但我理解你的观点。 - Ed James

1
如果Holding类中的属性(symbol、price等)是相同类型,您可以执行以下操作:
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);
    }
}

然后显示:

原始值:
数量 = 2,价格 = 5
数量 = 7,价格 = 2
数量 = 1,价格 = 3

按价格排序的值:
数量 = 7,价格 = 2
数量 = 1,价格 = 3
数量 = 2,价格 = 5


我喜欢Func的想法。Holding类中的属性是不同类型的,所以我必须采取修改后的方法来实现你所建议的功能,但无论如何都是个好主意。 - code4life
我刚刚看到其他回答,当我在写我的时候,你可以将查找更改为lookup = new Dictionary<PortfolioSheetMapping,Func<Holding,object>>,这应该可以工作。 - Matt Warren

1

我认为我们可以立即进行两个改进:

  • 使用frm.SortAscending来决定OrderByOrderByDesccending之间的逻辑在每个case中都有重复,可以将其提取到switch之后,如果case只是用于建立排序键并将其放入Func中。

  • 当然,这仍然留下了switch本身 - 可以用静态映射(例如在Dictionary中)从PortfolioSheetMapping到一个Func,该函数接受一个Holding并返回排序键。


谢谢,这些是很好的建议,肯定有助于更好地模块化代码。 - code4life

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接