按照枚举值对 List<T> 进行排序,其中枚举值是无序的。

31

我有一份消息列表,每个消息都有一个类型。

public enum MessageType
{
    Foo = 0,
    Bar = 1,
    Boo = 2,
    Doo = 3
}

枚举名称是任意的,不能更改。

我需要将列表按以下顺序排序:Boo、Bar、Foo、Doo。

我的当前解决方案是创建一个 tempList,在我想要的顺序中添加值,然后返回新列表。

List<Message> tempList = new List<Message>();
tempList.AddRange(messageList.Where(m => m.MessageType == MessageType.Boo));
tempList.AddRange(messageList.Where(m => m.MessageType == MessageType.Bar));
tempList.AddRange(messageList.Where(m => m.MessageType == MessageType.Foo));
tempList.AddRange(messageList.Where(m => m.MessageType == MessageType.Doo));
messageList = tempList;

我怎样可以用IComparer来完成这个任务?


2
编写一个新的IComparer,其中包含一个Compare方法,该方法根据您想要的枚举顺序返回-1、0和1。应该相当简单,您遇到了什么问题? - Robert Rouhani
是的,Robert有它。首先,您需要创建一个实现IComparer<MessageType>接口的对象,并具有具有签名int Compare(MessageType first, MessageType second)的方法。接下来,填写该方法,使其根据您想要的顺序返回-1、0和1。如果您在实现该方法时遇到问题,请发布您目前的进展以及为什么无法工作。 - Pete Baughman
7个回答

44

使用排序字典是使用 IComparer 的一种替代方法。

var orderMap = new Dictionary<MessageType, int>() {
    { MessageType.Boo, 0 },
    { MessageType.Bar, 1 },
    { MessageType.Foo, 2 },
    { MessageType.Doo, 3 }
};

var orderedList = messageList.OrderBy(m => orderMap[m.MessageType]);

7
有趣的是,他现在拥有的是桶排序,而且在渐进复杂度上比OrderBy(我很确定它必须是基于比较的)更快。这种方法更具可扩展性和易于维护,但如果他正在处理大数据集,则应注意时间复杂度。 - rliu
@roliu:我提出了一个不同的解决方案,我认为它可以解决维护和性能方面的问题。 - recursive
@voithos 我知道这是比较旧的问题,但如果字典中有第五种类型,会发生什么情况呢?它会作为第四种放在最后面,还是作为-1放在最前面,或者直接报错? - WWZee
2
@WayneO:在这种情况下,它会出现错误。因为 OrderBy 直接访问字典中的值(而不是使用类似于 此 GetValueOrDefault 扩展方法),当查找失败时,它将抛出 KeyNotFoundException。为了解决这个问题,您可以更改 lambda 表达式,使用类似于上面提到的 GetValueOrDefaultm => orderMap.GetValueOrDefault(m.MessageType, -1) - voithos
1
@voithos 这种方法与 GetValueOrDefault 一起使用非常好,因为您可以决定如何处理值和默认值。当枚举扩大时,默认值会在开头或结尾处给出默认位置。或者,如果许多枚举值不重要并且在中间优先考虑,则也可以这样做。很棒! - ZoolWay

22

那么,让我们编写自己的比较器:

public class MyMessageComparer : IComparer<MessageType> {
    protected IList<MessageType> orderedTypes {get; set;}

    public MyMessageComparer() {
        // you can reorder it's all as you want
        orderedTypes = new List<MessageType>() {
            MessageType.Boo,
            MessageType.Bar,
            MessageType.Foo,
            MessageType.Doo,
        };
    }

    public int Compare(MessageType x, MessageType y) {
        var xIndex = orderedTypes.IndexOf(x);
        var yIndex = orderedTypes.IndexOf(y);

        return xIndex.CompareTo(yIndex);
    }
};

如何使用:
messages.OrderBy(m => m.MessageType, new MyMessageComparer())

有一个更简单的方法:只需创建orderTypes列表并使用OrderBy的另一个重载即可:

var orderedTypes = new List<MessageType>() {        
            MessageType.Boo,
            MessageType.Bar,
            MessageType.Foo,
            MessageType.Doo,
    };

messages.OrderBy(m => orderedTypes.IndexOf(m.MessageType)).ToList();

嗯...让我们尝试从编写自己的IComparer中获得优势。思路:像我们上一个示例那样编写,但在某些其他语义上。就像这样:

messages.OrderBy(
      m => m.MessageType, 
      new EnumComparer<MessageType>() { 
          MessageType.Boo, 
          MessageType.Foo }
);

或者这个:
messages.OrderBy(m => m.MessageType, EnumComparer<MessageType>());

好的,我们需要什么。我们需要自己的比较器:

  1. 必须将枚举作为泛型类型进行接受(如何解决
  2. 必须能够与集合初始化语法一起使用(如何做到
  3. 当在我们的比较器中没有枚举值或某些枚举值不在我们的比较器中时,必须按默认顺序排序。

所以,这是代码:

public class EnumComparer<TEnum>: IComparer<TEnum>, IEnumerable<TEnum> where TEnum: struct, IConvertible {
    protected static IList<TEnum> TypicalValues { get; set; }

    protected IList<TEnum> _reorderedValues;

    protected IList<TEnum> ReorderedValues { 
        get { return _reorderedValues.Any() ? _reorderedValues : TypicalValues; } 
        set { _reorderedValues = value; }
    } 

    static EnumComparer() {
        if (!typeof(TEnum).IsEnum) 
        {
            throw new ArgumentException("T must be an enumerated type");
        }

        TypicalValues = new List<TEnum>();
        foreach (TEnum value in Enum.GetValues(typeof(TEnum))) {
            TypicalValues.Add(value);
        };            
    }

    public EnumComparer(IList<TEnum> reorderedValues = null) {
        if (_reorderedValues == null ) {
            _reorderedValues = new List<TEnum>();

            return;
        }

        _reorderedValues = reorderedValues;
    }

    public void Add(TEnum value) {
        if (_reorderedValues.Contains(value))
            return;

        _reorderedValues.Add(value);
    }

    public int Compare(TEnum x, TEnum y) {
        var xIndex = ReorderedValues.IndexOf(x);
        var yIndex = ReorderedValues.IndexOf(y);

        // no such enums in our order list:
        // so this enum values must be in the end
        //   and must be ordered between themselves by default

        if (xIndex == -1) {
            if (yIndex == -1) {
                xIndex = TypicalValues.IndexOf(x);
                yIndex = TypicalValues.IndexOf(y);
                return xIndex.CompareTo(yIndex);                
            }

           return -1;
        }

        if (yIndex == -1) {
            return -1; //
        }

        return xIndex.CompareTo(yIndex);
    }

    public void Clear() {
        _reorderedValues = new List<TEnum>();
    }

    private IEnumerable<TEnum> GetEnumerable() {
        return Enumerable.Concat(
            ReorderedValues,
            TypicalValues.Where(v => !ReorderedValues.Contains(v))
        );
    }

    public IEnumerator<TEnum> GetEnumerator() {
        return GetEnumerable().GetEnumerator();            
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerable().GetEnumerator();            
    }
}

好的,让我们让排序更快。 我们需要为我们的枚举类型覆盖默认的OrderBy方法:

public static class LinqEnumExtensions
{
    public static IEnumerable<TSource> OrderBy<TSource, TEnum>(this IEnumerable<TSource> source, Func<TSource, TEnum> selector, EnumComparer<TEnum> enumComparer) where TEnum : struct, IConvertible
    {
        foreach (var enumValue in enumComparer)
        {
            foreach (var sourceElement in source.Where(item => selector(item).Equals(enumValue)))
            {
                yield return sourceElement;
            }
        }
    }
}

是的,这很懒。你可以搜索一下yield的工作原理。好吧,让我们测试一下速度。简单的基准测试:http://pastebin.com/P8qaU20Y。n = 1000000时的结果;

Enumerable orderBy, elementAt: 00:00:04.5485845
       Own orderBy, elementAt: 00:00:00.0040010
Enumerable orderBy, full sort: 00:00:04.6685977
       Own orderBy, full sort: 00:00:00.4540575

我们发现,我们自己的orderBy比标准的orderBy更懒惰(是的,它不需要对所有内容进行排序)。即使是全排序,它也更快。

这段代码存在问题:它不支持ThenBy()。如果您需要此功能,可以编写自己的LINQ扩展程序,该程序返回IOrderedEnumerable。Jon Skeet撰写了一系列博客文章,深入介绍了LINQ to Objects,并提供了完整的替代实现。 IOrderedEnumerable的基础知识在第26a部分26b部分中有所涉及,更多细节和优化在26c部分26d部分中。


9

除了使用 IComparer 之外,您还可以使用 SelectMany 方法,如果您有固定数量的消息类型,则应该对大型消息列表具有更好的性能。

var messageTypeOrder = new [] {
    MessageType.Boo,
    MessageType.Bar,
    MessageType.Foo,
    MessageType.Doo,
};

List<Message> tempList = messageTypeOrder
    .SelectMany(type => messageList.Where(m => m.MessageType == type))
    .ToList();

很好。我想我会尝试更接近桶排序的写法,就像 messageTypeOrder.Select(t => messageList.Where(m => m.MessageType == t).SelectMany(b => b),但这不是什么大问题。如果你尝试将这段代码提交到工作中,可能更重要的是你会得到什么样的审查结果 :) - rliu
@roliu:诚实的问题:这与桶排序有什么关系?考虑到Linq的惰性特性,我看不出有什么区别。 - recursive
这可能只是一种偏好。我认为桶排序的步骤是:1)将列表分成桶,2)合并桶。我认为LINQ从左到右/从上到下。所以在你的代码中,我看到一个将桶作为输入的合并(2然后1)。在我的代码中,我先看到桶,然后再看到合并(1然后2)。我猜你的方式实际上更符合LINQ的精神,因为它更数学化(最外层的函数是最后一步),而我的方式更加命令式/可能更熟悉新手。 - rliu
嘿,大家好,为什么我们不能制作一个懒惰版本的桶排序呢?真正的问题是我不知道如何覆盖linq OrderBy方法。 - Viktor Lova

2
您可以避免编写全新的类型来实现IComparable。相反,使用Comparer类:
IComparer<Message> comparer = Comparer.Create<Message>((message) =>
    {
    // lambda that compares things
    });
tempList.Sort(comparer);

1
你可以使用LINQEnum值动态构建映射字典,如下所示:
  var mappingDIctionary = new List<string>((string[])Enum.GetNames(typeof(Hexside)))
                    .OrderBy(label => label )
                    .Select((i,n) => new {Index=i, Label=n}).ToList();

现在,将来添加到枚举中的任何新值都将自动正确映射。此外,如果有人决定重新编号、重构或重新排序枚举,一切都会自动处理。
更新:如下所述,没有要求按字母顺序排序;相反是半字母顺序排序,因此基本上是随机的。虽然这不是针对这个特定问题的答案,但这种技术可能对未来的访问者有用,因此我将保留它。

那个mappingDictionary并不是真正的“Dictionary”,它是一个动态对的“List”。此外,如果我没记错,OP要求在“Bar”之前排序“Boo”,但它们并没有按字母顺序排序。 - voithos

0

如果您想要使用实体框架(EF)使其工作,您需要将枚举值分散在OrderBy中,如下所示:

messageList.OrderBy(m => 
    m.MessageType == MessageType.Boo ? 0 :
    m.MessageType == MessageType.Bar ? 1 :
    m.MessageType == MessageType.Foo ? 2 :
    m.MessageType == MessageType.Doo ? 3 : 4
);

这将使用CASE WHEN创建一个子查询,然后在该临时列上进行ORDER BY


0

不需要进行映射。这将根据枚举值为您提供列表和顺序。即使更改枚举的顺序或添加新项,您也无需修改任何内容...

var result = (from x in tempList
              join y in Enum.GetValues(typeof(MessageType)).Cast<MessageType>()
              on x equals y
              orderby y
              select y).ToList();

这并没有回答问题,因为它会按照枚举顺序进行排序(Foo,Bar,Boo,Doo)- 问题是如何给它们一个自定义顺序(Boo,Barr,Foo,Doo)。 - ZoolWay

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