Ordenar una lista por enumeración donde la enumeración está fuera de orden

Tengo una lista de mensajes. Cada mensaje tiene un tipo.

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

Los nombres de enumeración son arbitrarios y no se pueden cambiar.

Necesito devolver la lista ordenada como: Boo, Bar, Foo, Doo

Mi solución actual es crear una lista temporal, agregar los valores en el orden que quiero y devolver la nueva lista.

 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; 

¿Cómo puedo hacer esto con un IComparer ?

Entonces, escribamos nuestro propio comparador:

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

Cómo utilizar:

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

Hay una forma más sencilla: simplemente cree la lista ordereTypes y use otra sobrecarga de OrderBy:

 var orderedTypes = new List() { MessageType.Boo, MessageType.Bar, MessageType.Foo, MessageType.Doo, }; messages.OrderBy(m => orderedTypes.IndexOf(m.MessageType)).ToList(); 

Hm … Intentemos aprovechar las ventajas de escribir nuestro propio IComparer. Idea: escríbelo como nuestro último ejemplo pero en algún otro semántico. Me gusta esto:

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

O esto:

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

Está bien, así que lo que necesitamos. Nuestro propio comparador:

  1. Debe aceptar la enumeración como tipo genérico ( cómo resolver )
  2. Debe ser utilizable con la syntax del inicializador de colección ( cómo hacerlo )
  3. Debe ordenarse por orden predeterminado, cuando no tenemos valores de enumeración en nuestro comparador (o algunos valores de enumeración no están en nuestro comparador)

Entonces, aquí está el código:

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

Entonces, bueno, hagamos la clasificación más rápido. Necesitamos anular el método predeterminado OrderBy para nuestras enumeraciones:

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

Sí, eso es perezoso. Puedes google cómo funciona el rendimiento. Bueno, vamos a probar la velocidad. Punto de referencia simple: http://pastebin.com/P8qaU20Y . Resultado para 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 

Vemos, que nuestro propio ordenBy por es más perezoso que el estándar por orden (sí, no es necesario ordenar todo). Y más rápido incluso para fullsort.

Problemas en este código: no es compatible con ThenBy() . Si necesita esto, puede escribir su propia extensión de linq que devuelve IOrderedEnumerable Hay una serie de publicaciones de blog de Jon Skeet que se refiere a LINQ to Objects con cierta profundidad, lo que brinda una implementación alternativa completa. La base de IOrderedEnumerable se cubre en las partes 26a y 26b , con más detalles y optimización en 26c y 26d .

Una alternativa al uso de IComparer sería construir un diccionario de pedidos.

 var orderMap = new Dictionary() { { MessageType.Boo, 0 }, { MessageType.Bar, 1 }, { MessageType.Foo, 2 }, { MessageType.Doo, 3 } }; var orderedList = messageList.OrderBy(m => orderMap[m.MessageType]); 

En lugar de usar un IComparer , también podría usar un enfoque SelectMany , que debería tener un mejor rendimiento para listas de mensajes grandes, si tiene un número fijo de tipos de mensajes.

 var messageTypeOrder = new [] { MessageType.Boo, MessageType.Bar, MessageType.Foo, MessageType.Doo, }; List tempList = messageTypeOrder .SelectMany(type => messageList.Where(m => m.MessageType == type)) .ToList(); 

Puede evitar escribir un tipo completamente nuevo solo para implementar IComparable. Utilice la clase Comparer en su lugar:

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

Puede crear un diccionario de mapeo dinámicamente a partir de los valores de Enum con LINQ de esta manera:

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

Ahora, cualquier nuevo valor agregado al futuro Enum n se asignará automáticamente.

Además, si alguien decide renumerar, refactorizar o reordenar la enumeración, todo se maneja automáticamente.

Actualización: Como se indica a continuación, no se solicitó el orden alfabético; Más bien un ordenamiento semi-alfabético, tan esencialmente aleatorio. Aunque no es una respuesta a esta pregunta en particular, esta técnica podría ser útil para futuros visitantes, por lo que la dejaré en pie.

No es necesario tener el mapeo. Esto debería darle la lista y el orden basado en la enumeración. No tiene que modificar nada, incluso cuando cambia el orden de la enumeración o nuevos elementos …

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

Si está a punto de conseguir que esto funcione con Entity Framework (EF), tendría que extender su enumeración en su OrderBy como tal:

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

Esto crea una selección secundaria con CASE WHEN , luego ORDER BY esa columna temporal.