it-swarm.com.de

Sortieren Sie eine Liste <T> nach Aufzählung, wenn die Aufzählung nicht in der richtigen Reihenfolge ist

Ich habe eine Liste mit Nachrichten. Jede Nachricht hat einen Typ.

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

Die Aufzählungsnamen sind willkürlich und können nicht geändert werden.

Ich muss die Liste sortiert zurücksenden: Boo, Bar, Foo, Doo

Meine derzeitige Lösung besteht darin, eine TempList zu erstellen, die Werte in der gewünschten Reihenfolge hinzuzufügen und die neue Liste zurückzugeben.

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;

Wie mache ich das mit einem IComparer ?

25
jpiccolo

Schreiben wir also unseren eigenen Vergleicher:

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);
    }
};

Wie benutzt man:

messages.OrderBy(m => m.MessageType, new MyMessageComparer())

Es gibt einen einfacheren Weg: Erstellen Sie einfach eine ordereTypes-Liste und verwenden Sie eine weitere Überladung von OrderBy:

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

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

Hm .. Versuchen wir, die Vorteile unseres eigenen IComparers zu nutzen. Idee: Schreiben Sie es wie unser letztes Beispiel, aber in einer anderen Semantik. So was:

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

Oder dieses: 

messages.OrderBy(m => m.MessageType, EnumComparer<MessageType>());

Okay, also was brauchen wir. Unser eigener Vergleicher:

  1. Muss Enum als generischen Typ akzeptieren ( wie löse ich )
  2. Muss mit der Syntax des Collection-Initialisierers verwendbar sein ( how to )
  3. Muss nach Standardreihenfolge sortieren, wenn wir keine Enumerationswerte in unserem Vergleicher haben (oder einige Enumerationswerte nicht in unserem Vergleicher sind)

Also hier ist der Code:

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();            
    }
}

Also, lasst uns das Sortieren schneller machen. Wir müssen die standardmäßige OrderBy-Methode für unsere Aufzählungen überschreiben:

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;
            }
        }
    }
}

Ja, das ist faul. Sie können google, wie der Ertrag funktioniert. Nun, testen wir die Geschwindigkeit. Einfacher Benchmark: http://Pastebin.com/P8qaU20Y . Ergebnis für 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

Wir sehen, dass unsere eigene OrderBy by fauler ist als die Standart Order (ja, es muss nicht alles sortiert werden). Und noch schneller für Fullsort.

Probleme in diesem Code: ThenBy() wird nicht unterstützt. Wenn Sie dies benötigen, können Sie eine eigene linq-Erweiterung schreiben, die IOrderedEnumerable zurückgibt. Es gibt eine Blogpost-Serie von Jon Skeet , die in LINQ to Objects ausführlicher behandelt wird und eine vollständige alternative Implementierung bietet. Die Basis von IOrderedEnumerable wird in Teil 26a und 26b behandelt, mit mehr Details und Optimierung in 26c und 26d .

16
Viktor Lova

Eine Alternative zur Verwendung von IComparer wäre das Erstellen eines Bestellwörterbuchs.

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]);
37
voithos

Anstelle einer IComparer können Sie auch einen SelectMany-Ansatz verwenden, der bei großen Nachrichtenlisten eine bessere Leistung bieten sollte, wenn Sie über eine feste Anzahl von Nachrichtentypen verfügen.

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

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

Sie können vermeiden, einen völlig neuen Typ zu schreiben, nur um IComparable zu implementieren. Verwenden Sie stattdessen die Comparer-Klasse:

IComparer<Message> comparer = Comparer.Create<Message>((message) =>
    {
    // lambda that compares things
    });
tempList.Sort(comparer);
2

Sie können ein Mapping-Wörterbuch dynamisch aus den Enum-Werten mitLINQerstellen:

  var mappingDIctionary = new List<string>((string[])Enum.GetNames(typeof(Hexside)))
                    .OrderBy(label => label )
                    .Select((i,n) => new {Index=i, Label=n}).ToList();

Jetzt werden alle neuen Werte, die der Enum n future hinzugefügt werden, automatisch richtig zugeordnet.

Wenn sich jemand dafür entscheidet, die Aufzählung neu zu nummerieren, umzuformen oder neu zu ordnen, wird alles automatisch behandelt.

Update: Wie unten ausgeführt, war keine alphabetische Anordnung erforderlich. eher eine halb alphabetische Reihenfolge, also im Wesentlichen zufällig. Diese Technik ist zwar keine Antwort auf diese spezielle Frage, kann jedoch für zukünftige Besucher von Nutzen sein, also werde ich sie stehen lassen.

1
Pieter Geerkens

Wenn Sie dies mit dem Entity Framework (EF) zum Laufen bringen möchten, müssen Sie Ihr Enum in Ihrer OrderBy als solches verteilen:

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

Dadurch wird eine Unterauswahl mit CASE WHEN und dann ORDER BY in dieser temporären Spalte erstellt.

0
jsgoupil

Kein Mapping erforderlich. Dies sollte Ihnen die Liste und Reihenfolge basierend auf der Aufzählung geben. Sie müssen nichts ändern, auch wenn Sie die Reihenfolge der Enummen oder neue Elemente ändern.

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