J'ai une liste de messages. Chaque message a un type.
public enum MessageType
{
Foo = 0,
Bar = 1,
Boo = 2,
Doo = 3
}
Les noms d'énumération sont arbitraires et ne peuvent pas être modifiés.
Je dois renvoyer la liste triée comme suit: Boo, Bar, Foo, Doo
Ma solution actuelle consiste à créer une liste tempList, à ajouter les valeurs dans l'ordre de mon choix et à renvoyer la nouvelle liste.
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;
Comment puis-je faire cela avec un IComparer ?
Alors, écrivons notre propre comparateur:
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);
}
};
Comment utiliser:
messages.OrderBy(m => m.MessageType, new MyMessageComparer())
Il existe un moyen plus simple: il suffit de créer la liste ordereTypes et d’utiliser une autre surcharge de OrderBy:
var orderedTypes = new List<MessageType>() {
MessageType.Boo,
MessageType.Bar,
MessageType.Foo,
MessageType.Doo,
};
messages.OrderBy(m => orderedTypes.IndexOf(m.MessageType)).ToList();
Hm .. Essayons de tirer parti de l'écriture de notre propre IComparer. Idée: écrivez-la comme notre dernier exemple, mais dans une autre sémantique. Comme ça:
messages.OrderBy(
m => m.MessageType,
new EnumComparer<MessageType>() {
MessageType.Boo,
MessageType.Foo }
);
Ou ca:
messages.OrderBy(m => m.MessageType, EnumComparer<MessageType>());
Ok, alors de quoi avons-nous besoin. Notre propre comparateur:
Alors, voici le 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();
}
}
Alors, bien, accélérons le tri. Nous devons remplacer la méthode OrderBy par défaut pour nos énumérations:
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;
}
}
}
}
Oui, c'est paresseux. Vous pouvez google comment le rendement fonctionne. Eh bien, testons la vitesse. Indice de référence simple: http://Pastebin.com/P8qaU20Y . Résultat pour 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
Nous voyons que notre propre ordrePar par est plus paresseux que l’ordre standart par (ouais, il n’a pas besoin de tout trier). Et plus vite même pour le tri complet.
Problèmes dans ce code: il ne supporte pas ThenBy()
. Si vous en avez besoin, vous pouvez écrire votre propre extension linq qui retourne IOrderedEnumerable
. Il existe une série de messages blog de Jon Skeet qui va dans LINQ to Objects de manière assez approfondie, offrant une autre implémentation complète. La base de IOrderedEnumerable
est traitée dans la partie 26a et 26b , avec plus de détails et une optimisation dans 26c et 26d .
Une alternative à l'utilisation de IComparer
serait de créer un dictionnaire de commande.
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]);
Au lieu d'utiliser une IComparer
, vous pouvez également utiliser une approche SelectMany
, qui devrait offrir de meilleures performances pour les listes de messages volumineuses, si vous avez un nombre fixe de types de message.
var messageTypeOrder = new [] {
MessageType.Boo,
MessageType.Bar,
MessageType.Foo,
MessageType.Doo,
};
List<Message> tempList = messageTypeOrder
.SelectMany(type => messageList.Where(m => m.MessageType == type))
.ToList();
Vous éviterez peut-être d'écrire un type complètement nouveau juste pour implémenter IComparable. Utilisez plutôt la classe Comparer:
IComparer<Message> comparer = Comparer.Create<Message>((message) =>
{
// lambda that compares things
});
tempList.Sort(comparer);
Vous pouvez créer un dictionnaire de mappage de manière dynamique à partir des valeurs Enum
avecLINQcomme ceci:
var mappingDIctionary = new List<string>((string[])Enum.GetNames(typeof(Hexside)))
.OrderBy(label => label )
.Select((i,n) => new {Index=i, Label=n}).ToList();
Désormais, toute nouvelle valeur ajoutée au futur Enum n sera automatiquement correctement mappée.
De plus, si quelqu'un décide de renuméroter, de refactoriser ou de réorganiser l'énumération, tout est traité automatiquement.
Mise à jour: Comme indiqué ci-dessous, l'ordre alphabétique n'était pas demandé; plutôt un ordre semi-alphabétique, donc essentiellement aléatoire. Bien que n'étant pas une réponse à cette question particulière, cette technique pourrait être utile aux futurs visiteurs, je la laisserai donc debout.
Si vous êtes sur le point de faire fonctionner cela avec Entity Framework (EF), vous devez étaler votre enum dans votre OrderBy
en tant que tel:
messageList.OrderBy(m =>
m.MessageType == MessageType.Boo ? 0 :
m.MessageType == MessageType.Bar ? 1 :
m.MessageType == MessageType.Foo ? 2 :
m.MessageType == MessageType.Doo ? 3 : 4
);
Cela crée une sous-sélection avec CASE WHEN
, puis ORDER BY
sur cette colonne temporaire.
Pas besoin d'avoir la cartographie. Cela devrait vous donner la liste et l'ordre en fonction de l'énum. Vous n'avez rien à modifier même lorsque vous modifiez la commande de l'énum ou de nouveaux éléments ...
var result = (from x in tempList
join y in Enum.GetValues(typeof(MessageType)).Cast<MessageType>()
on x equals y
orderby y
select y).ToList();