Сортировка списка <T> по перечислению, где перечисление не соответствует порядку
У меня есть список сообщений.
Каждое сообщение имеет тип.
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?
Ответы
Ответ 1
Альтернативой использованию 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]);
Ответ 2
Итак, напишите наш собственный компаратор:
public class MyMessageComparer : IComparer<MessageType> {
protected IList<MessageType> orderedTypes {get; set;}
public MyMessageComparer() {
// you can reorder it 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())
Существует более простой способ: просто создайте список ordereTypes и используйте другую перегрузку OrderBy:
var orderedTypes = new List<MessageType>() {
MessageType.Boo,
MessageType.Bar,
MessageType.Foo,
MessageType.Doo,
};
messages.OrderBy(m => orderedTypes.IndexOf(m.MessageType)).ToList();
Hm.. Попробуем воспользоваться преимуществами написания собственного IComparer. Идея: напишите это как наш последний пример, но в какой-то другой семантике. Вот так:
messages.OrderBy(
m => m.MessageType,
new EnumComparer<MessageType>() {
MessageType.Boo,
MessageType.Foo }
);
Или это:
messages.OrderBy(m => m.MessageType, EnumComparer<MessageType>());
Хорошо, так что нам нужно. Наш собственный компаратор:
- Необходимо принять перечисление как общий тип (как решить)
- Должен использоваться с синтаксисом инициализатора коллекции (как)
- Должен сортировать по умолчанию порядок, когда у нас нет значений перечисления в нашем компаменте (или некоторые значения перечисления не находятся в нашем компаменте)
Итак, вот код:
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;
}
}
}
}
Да, это лениво. Вы можете 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 к объектам в некоторой степени, обеспечивая полную альтернативную реализацию. Основание IOrderedEnumerable
описано в часть 26a и 26b, с более подробной информацией и оптимизацией в 26c и 26d.
Ответ 3
Вместо использования 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();
Ответ 4
Вы можете избежать написания совершенно нового типа только для реализации IComparable. Вместо этого используйте класс Comparer:
IComparer<Message> comparer = Comparer.Create<Message>((message) =>
{
// lambda that compares things
});
tempList.Sort(comparer);
Ответ 5
Вы можете динамически строить словарь сопоставления из значений Enum
с помощью LINQ следующим образом:
var mappingDIctionary = new List<string>((string[])Enum.GetNames(typeof(Hexside)))
.OrderBy(label => label )
.Select((i,n) => new {Index=i, Label=n}).ToList();
Теперь любые новые значения, добавленные в будущее Enum, автоматически будут правильно отображаться.
Кроме того, если кто-то решает изменить нумерацию, рефакторинг или изменить порядок перечисления, все будет обрабатываться автоматически.
Update:
Как указано ниже, Алфавитный порядок не призывался; скорее полу-алфавитным упорядочением, так что существенно случайным. Хотя это и не ответ на этот конкретный вопрос, этот метод может быть полезен будущим посетителям, поэтому я оставлю его стоящим.
Ответ 6
Нет необходимости иметь сопоставление. Это должно дать вам список и порядок на основе перечисления. Вам не нужно ничего изменять, даже если вы изменяете порядок перечисления и/или новые элементы...
var result = (from x in tempList
join y in Enum.GetValues(typeof(MessageType)).Cast<MessageType>()
on x equals y
orderby y
select y).ToList();