Сортувати список <T> за enum, де перерахування не виконується

У мене є список повідомлень. Кожне повідомлення має тип.

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

Імена переліку є довільними і не можуть бути змінені.

Мені потрібно повернути список, відсортований як: Boo, Bar, Foo, Doo

Моє поточне рішення - створити tempList, додати значення у порядку, який я хочу, повернути новий список.

List tempList = new List();
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 ?

25
Так, у Роберта є. Спочатку потрібно створити об'єкт, який реалізує IComparer за допомогою методу, який має підпис int Compare (MessageType спочатку, MessageType second). замовлення, яке ви хочете. Якщо у вас виникли проблеми з реалізацією цього методу, публікуйте те, що ви придумали до цих пір, і чому він не працює
додано Автор Pete Baughman, джерело
Напишіть новий IComparer за допомогою методу Compare, який повертає -1, 0 і 1 на основі порядку потрібних переліків. Має бути досить простим, з чим у вас виникли проблеми?
додано Автор Robert Rouhani, джерело

7 Відповіді

Альтернативою використання коду IComparer є створення словника впорядкування.

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

var orderedList = messageList.OrderBy(m => orderMap[m.MessageType]);
37
додано
@voithos Я знаю, що це старше, але що станеться, якщо був 5-й тип, не включений до словника? Чи буде вона йти в спину як 4, або фронт як -1, або просто помилка?
додано Автор Wayne O, джерело
@roliu: Я запропонував інше рішення, яке, на мою думку, стосується проблем, пов'язаних з технічним обслуговуванням та продуктивністю.
додано Автор recursive, джерело
@Kevin: Так, приємний улов.
додано Автор voithos, джерело
@WayneO: У цьому випадку буде помилка. Оскільки OrderBy безпосередньо отримує доступ до значень зі словника (замість, наприклад, використовуючи щось подібне цьому методу розширення GetValueOrDefault ) він викине KeyNotFoundException під час невдалого пошуку. Щоб пояснити це, ви можете змінити лямбда, щоб використовувати щось схоже на GetValueOrDefault , згадане вище: m => orderMap.GetValueOrDefault (m.MessageType, -1) .
додано Автор voithos, джерело
@voithos Цей підхід разом з GetValueOrDefault є дуже приємним підходом, оскільки ви можете вирішити, як обробляти значення та типові значення. Коли перелік буде збільшено, типовим буде значення за замовчуванням на початку або в кінці. Або якщо багато значень перерахування не мають значення і є пріоритетними в середині, ви можете це зробити. Приємно!
додано Автор ZoolWay, джерело
Найсмішніше те, що те, що він має зараз, - це сорт відро, і це асимптотично швидше, ніж OrderBy (оскільки я впевнений, що він повинен бути порівняним). Щось на зразок цього є більш розширюваним і простішим у обслуговуванні, але складність часу може бути варто відзначити, якщо він обробляє великі набори даних.
додано Автор rliu, джерело
має бути новий Словник (), чи не так?
додано Автор Kevin, джерело

Отже, напишемо наш власний Comparer:

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

    public MyMessageComparer() {
       //you can reorder it's all as you want
        orderedTypes = new List() {
            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())

Існує більш простий спосіб: просто створіть список ordereTypes і використовуйте інше перевантаження OrderBy:

var orderedTypes = new List() {        
            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.Boo, 
          MessageType.Foo }
);

Або це:

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

Добре, так що нам потрібно. Наш власний порівняльник:

  1. Необхідно прийняти enum як загальний тип ( як вирішити )
  2. Потрібно використовувати з синтаксисом ініціалізації колекції ( як )
  3. Необхідно відсортувати за замовчуванням, якщо у нашому порівнянні немає значень перерахування (або деякі значення перерахування не знаходяться в нашому порівнянні)

Отже, ось код:

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

    protected IList _reorderedValues;

    protected IList 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();
        foreach (TEnum value in Enum.GetValues(typeof(TEnum))) {
            TypicalValues.Add(value);
        };            
    }

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

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

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

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

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

Отже, давайте зробимо сортування швидше. Нам потрібно перевизначити метод замовлення за замовчуванням для наших переліків:

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

Так, це ліниво. Ви можете Google, як вихід працює. Ну, давайте перевіримо швидкість. Простий тест: 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

Ми бачимо, що наше власне розпорядження за лінішим, що стандартний порядок (так, не потрібно все сортувати). І швидше навіть для повноїсертифікації.

Проблеми в цьому коді: не підтримує ThenBy() . Якщо вам це потрібно, ви можете написати власне розширення linq, яке повертає IOrderedEnumerable Є серія блогу від Jon Skeet , яка надходить у LINQ to Objects на певній глибині, забезпечуючи повну альтернативну реалізацію. Основа IOrderedEnumerable висвітлюється у частина 26a і 26b , з більш детальною інформацією та оптимізацією в 26c і 26d .

15
додано

Замість того, щоб використовувати IComparer , ви також можете використовувати підхід SelectMany , який повинен мати кращу продуктивність для великих списків повідомлень, якщо у вас є фіксоване число типів повідомлень.

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

List tempList = messageTypeOrder
    .SelectMany(type => messageList.Where(m => m.MessageType == type))
    .ToList();
8
додано
@roliu: Чесне запитання: як це ближче до сорту відра? Враховуючи ледачий характер linq, я не бачу, в чому різниця.
додано Автор recursive, джерело
Cool. Я думаю, що я спробую написати його ближче до сортування, так як messageTypeOrder.Select (t => messageList.Where (m => m.MessageType == t) .SelectMany (b => b)
додано Автор rliu, джерело
Це, мабуть, просто перевага. Я думаю про кроки відра, як: 1) Розділити список у відра 2) Об'єднати відра. Я думаю, що LINQ зліва направо/зверху вниз. Таким чином, у вашому коді, я бачу злиття, яке приймає відра як вхідний (2, то 1). У моєму коді я бачу відра, а потім злиття (1, потім 2). Думаю, ваш шлях насправді більш вірний духу LINQ в тому, що він більш математичний (зовнішня функція є останнім «кроком»), тоді як моя більш нагальна/може бути більш знайома новачкам.
додано Автор rliu, джерело
Гей, хлопці, чому ми не можемо зробити ледачу версію сорту ковша? Реальна проблема, яку я не знаю, як перекрити метод linq OrderBy
додано Автор Viktor Lova, джерело

Ви можете уникнути написання абсолютно нового типу лише для реалізації IComparable. Замість цього використовуйте клас Comparer:

IComparer comparer = Comparer.Create((message) =>
    {
   //lambda that compares things
    });
tempList.Sort(comparer);
2
додано

Ви можете побудувати словник зіставлення динамічно зі значень Enum з LINQ , як це:

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

Тепер будь-які нові значення, додані до майбутнього Enum n, автоматично отримуватимуться належним чином.

Крім того, якщо хтось вирішує перенумерувати, рефакторувати або змінити порядок перерахування, все обробляється автоматично.

Update: As pointed out below, Alphabetical ordering was not called for; rather a semi- alphabetical ordering, so essentially random. Although not an answer to this particular question, this technique might be useful to future visitors, so I will leave it standing.

1
додано
Це mappingDictionary насправді не є словник , це List динамічних пар. Крім того, якщо я не помиляюся, оператор попросив замовити Boo перед Bar , але вони не впорядковані за алфавітом.
додано Автор voithos, джерело

Якщо ви збираєтеся працювати з Entity Framework (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 , потім ORDER BY на тимчасовому стовпці.

0
додано

Немає необхідності мати відображення. Це повинно дати вам список і порядок на основі перерахування. Вам не потрібно нічого змінювати, навіть коли ви змінюєте порядок перерахування чи та нові елементи ...

var result = (from x in tempList
              join y in Enum.GetValues(typeof(MessageType)).Cast()
              on x equals y
              orderby y
              select y).ToList();
0
додано
Це не відповідає на питання, як би він сортувався за порядком перерахування (Foo, Bar, Boo, Doo) - питання полягало в тому, як надати їм індивідуальне замовлення (Boo, Barr, Foo, Doo).
додано Автор ZoolWay, джерело
var chat = new Chat();
var chat = new Chat();
642 учасників

Обсуждение вопросов по C# / .NET / .NET Core / .NET Standard / Azure Сообщества-организаторы: — @itkpi — @dncuug