Sortieren einer Liste mit Lambda / Linq nach Objekten

274

Ich habe den Namen der "nach Eigenschaft sortieren" in einer Zeichenfolge. Ich muss Lambda / Linq verwenden, um die Liste der Objekte zu sortieren.

Ex:

public class Employee
{
  public string FirstName {set; get;}
  public string LastName {set; get;}
  public DateTime DOB {set; get;}
}


public void Sort(ref List<Employee> list, string sortBy, string sortDirection)
{
  //Example data:
  //sortBy = "FirstName"
  //sortDirection = "ASC" or "DESC"

  if (sortBy == "FirstName")
  {
    list = list.OrderBy(x => x.FirstName).toList();    
  }

}
  1. Anstatt eine Reihe von Wenns zum Überprüfen des Feldnamens (sortBy) zu verwenden, gibt es eine sauberere Methode zum Sortieren
  2. Ist sort der Datentyp bekannt?
DotnetDude
quelle
Ich sehe sortBy == "Vorname" . Wollte das OP stattdessen .Equals () ?
Pieter
3
@Pieter er wollte wahrscheinlich Gleichheit vergleichen, aber ich bezweifle, dass er ".Equals () tun wollte". Tippfehler führen normalerweise nicht zu funktionierendem Code.
C.Evenhuis
1
@Pieter Deine Frage macht nur Sinn, wenn du denkst, dass etwas nicht stimmt mit ==... was?
Jim Balter

Antworten:

365

Dies kann als erfolgen

list.Sort( (emp1,emp2)=>emp1.FirstName.CompareTo(emp2.FirstName) );

Das .NET Framework gießt das Lambda (emp1,emp2)=>intalsComparer<Employee>.

Dies hat den Vorteil, stark typisiert zu sein.

gls123
quelle
Es ist mir oft passiert, komplexe Vergleichsoperatoren zu schreiben, die mehrere Vergleichskriterien und letztendlich einen ausfallsicheren GUID-Vergleich beinhalten, um Antisymmetrie sicherzustellen. Würden Sie einen Lambda-Ausdruck für einen solchen komplexen Vergleich verwenden? Wenn nicht, bedeutet dies, dass Lambda-Expressionsvergleiche nur auf einfache Fälle beschränkt sein sollten?
Simone
4
Ja, ich sehe es auch nicht so? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null))) ;;
Sa
1
wie man umgekehrt sortiert?
JerryGoyal
1
@JerryGoyal tauschen Sie die Parameter aus ... emp2.FirstName.CompareTo (emp1.FirstName) usw.
Chris Hynes
3
Nur weil es sich um eine Funktionsreferenz handelt, muss es sich nicht um einen Einzeiler handeln. Sie könnten einfach schreibenlist.sort(functionDeclaredElsewhere)
The Hoff
74

Eine Sache, die Sie tun könnten, ist zu ändern, Sortdamit Lambdas besser genutzt werden.

public enum SortDirection { Ascending, Descending }
public void Sort<TKey>(ref List<Employee> list,
                       Func<Employee, TKey> sorter, SortDirection direction)
{
  if (direction == SortDirection.Ascending)
    list = list.OrderBy(sorter);
  else
    list = list.OrderByDescending(sorter);
}

Jetzt können Sie das Feld angeben, das beim Aufrufen der SortMethode sortiert werden soll.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Samuel
quelle
7
Da sich die Sortierspalte in einer Zeichenfolge befindet, benötigen Sie immer noch einen Schalter / if-else-Block, um zu bestimmen, welche Funktion übergeben werden soll.
Tvanfosson
1
Sie können diese Annahme nicht machen. Wer weiß, wie sein Code es nennt.
Samuel
3
Er stellte in der Frage fest, dass die "Sortierung nach Eigenschaft" in einer Zeichenfolge steht. Ich gehe nur auf seine Frage ein.
Tvanfosson
6
Ich denke, es ist wahrscheinlicher, weil es von einem Sortiersteuerelement auf einer Webseite stammt, das die Sortierspalte als Zeichenfolgenparameter zurückgibt. Das wäre sowieso mein Anwendungsfall.
Tvanfosson
2
@tvanfosson - Sie haben Recht, ich habe ein benutzerdefiniertes Steuerelement, das die Reihenfolge und den Feldnamen als Zeichenfolge hat
DotnetDude
55

Sie können Reflection verwenden, um den Wert der Eigenschaft abzurufen.

list = list.OrderBy( x => TypeHelper.GetPropertyValue( x, sortBy ) )
           .ToList();

Wobei TypeHelper eine statische Methode hat wie:

public static class TypeHelper
{
    public static object GetPropertyValue( object obj, string name )
    {
        return obj == null ? null : obj.GetType()
                                       .GetProperty( name )
                                       .GetValue( obj, null );
    }
}

Möglicherweise möchten Sie auch Dynamic LINQ aus der VS2008-Beispielbibliothek anzeigen . Sie können die IEnumerable-Erweiterung verwenden, um die Liste als IQueryable umzuwandeln, und dann die OrderBy-Erweiterung für dynamische Links verwenden.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
Tvanfosson
quelle
1
Dies löst zwar sein Problem, aber wir möchten ihn möglicherweise davon abhalten, eine Zeichenfolge zum Sortieren zu verwenden. Trotzdem eine gute Antwort.
Samuel
Sie können Dynamic Linq ohne Linq zu SQL verwenden, um das zu tun, was er braucht ... Ich liebe es
JoshBerke
Sicher. Sie können es in IQueryable konvertieren. Ich habe nicht darüber nachgedacht. Aktualisiere meine Antwort.
Tvanfosson
@Samuel Wenn die Sortierung als Routenvariable eingeht, gibt es keine andere Möglichkeit, sie zu sortieren.
Chev
1
@ChuckD - Bringen Sie die Sammlung in den Speicher, bevor Sie versuchen, sie zu verwenden, z. B.collection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson
20

So habe ich mein Problem gelöst:

List<User> list = GetAllUsers();  //Private Method

if (!sortAscending)
{
    list = list
           .OrderBy(r => r.GetType().GetProperty(sortBy).GetValue(r,null))
           .ToList();
}
else
{
    list = list
           .OrderByDescending(r => r.GetType().GetProperty(sortBy).GetValue(r,null))
           .ToList();
}
Cornel Urian
quelle
16

Das Erstellen der Reihenfolge durch Ausdruck kann hier gelesen werden

Schamlos von der Seite im Link gestohlen:

// First we define the parameter that we are going to use
// in our OrderBy clause. This is the same as "(person =>"
// in the example above.
var param = Expression.Parameter(typeof(Person), "person");

// Now we'll make our lambda function that returns the
// "DateOfBirth" property by it's name.
var mySortExpression = Expression.Lambda<Func<Person, object>>(Expression.Property(param, "DateOfBirth"), param);

// Now I can sort my people list.
Person[] sortedPeople = people.OrderBy(mySortExpression).ToArray();
Rashack
quelle
Damit sind folgende Probleme verbunden: DateTime-Sortierung.
CrazyEnigma
Wie wäre es auch mit zusammengesetzten Klassen, dh Person.Employer.CompanyName?
Davewilliams459
Ich habe im Wesentlichen das Gleiche getan und diese Antwort hat es gelöst.
Jason.Net
8

Sie können Reflection verwenden, um auf die Eigenschaft zuzugreifen.

public List<Employee> Sort(List<Employee> list, String sortBy, String sortDirection)
{
   PropertyInfo property = list.GetType().GetGenericArguments()[0].
                                GetType().GetProperty(sortBy);

   if (sortDirection == "ASC")
   {
      return list.OrderBy(e => property.GetValue(e, null));
   }
   if (sortDirection == "DESC")
   {
      return list.OrderByDescending(e => property.GetValue(e, null));
   }
   else
   {
      throw new ArgumentOutOfRangeException();
   }
}

Anmerkungen

  1. Warum übergeben Sie die Liste als Referenz?
  2. Sie sollten eine Aufzählung für die Sortierrichtung verwenden.
  3. Sie könnten eine viel sauberere Lösung erhalten, wenn Sie einen Lambda-Ausdruck übergeben würden, der die zu sortierende Eigenschaft anstelle des Eigenschaftsnamens als Zeichenfolge angibt.
  4. In meiner Beispielliste verursacht == null eine NullReferenceException. Sie sollten diesen Fall abfangen.
Daniel Brückner
quelle
Hat jemand anderes jemals bemerkt, dass dies ein ungültiger Rückgabetyp ist, der jedoch Listen zurückgibt?
emd
Zumindest kümmerte sich niemand darum, es zu reparieren, und ich bemerkte es nicht, weil ich den Code nicht mit einer IDE schrieb. Vielen Dank für den Hinweis.
Daniel Brückner
6

Sort verwendet die IComparable-Schnittstelle, sofern der Typ dies implementiert. Und Sie können das Wenn vermeiden, indem Sie einen benutzerdefinierten IComparer implementieren:

class EmpComp : IComparer<Employee>
{
    string fieldName;
    public EmpComp(string fieldName)
    {
        this.fieldName = fieldName;
    }

    public int Compare(Employee x, Employee y)
    {
        // compare x.fieldName and y.fieldName
    }
}

und dann

list.Sort(new EmpComp(sortBy));
Serguei
quelle
Zu Ihrer Information: Sortieren ist eine Methode von List <T> und keine Linq-Erweiterung.
Serguei
5

Antwort für 1.:

Sie sollten in der Lage sein, manuell einen Ausdrucksbaum zu erstellen, der unter Verwendung des Namens als Zeichenfolge an OrderBy übergeben werden kann. Oder Sie können die Reflexion verwenden, wie in einer anderen Antwort vorgeschlagen, was möglicherweise weniger Arbeit bedeutet.

Bearbeiten : Hier ist ein funktionierendes Beispiel für das manuelle Erstellen eines Ausdrucksbaums. (Sortieren nach X.Wert, wenn nur der Name "Wert" der Eigenschaft bekannt ist). Sie könnten (sollten) eine generische Methode dafür erstellen.

using System;
using System.Linq;
using System.Linq.Expressions;

class Program
{
    private static readonly Random rand = new Random();
    static void Main(string[] args)
    {
        var randX = from n in Enumerable.Range(0, 100)
                    select new X { Value = rand.Next(1000) };

        ParameterExpression pe = Expression.Parameter(typeof(X), "value");
        var expression = Expression.Property(pe, "Value");
        var exp = Expression.Lambda<Func<X, int>>(expression, pe).Compile();

        foreach (var n in randX.OrderBy(exp))
            Console.WriteLine(n.Value);
    }

    public class X
    {
        public int Value { get; set; }
    }
}

Um einen Ausdrucksbaum zu erstellen, müssen Sie jedoch die teilnehmenden Typen kennen. Dies kann in Ihrem Nutzungsszenario ein Problem sein oder auch nicht. Wenn Sie nicht wissen, nach welchem ​​Typ Sie sortieren sollen, ist die Verwendung von Reflektion wahrscheinlich einfacher.

Antwort für 2.:

Ja, da Comparer <T> .Default für den Vergleich verwendet wird, wenn Sie den Comparer nicht explizit definieren.

driis
quelle
Haben Sie ein Beispiel für die Erstellung eines Ausdrucksbaums, der an OrderBy übergeben werden soll?
DotnetDude
4
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;

public static class EnumerableHelper
{

    static MethodInfo orderBy = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderBy" && x.GetParameters().Length == 2).First();

    public static IEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, string propertyName)
    {
        var pi = typeof(TSource).GetProperty(propertyName, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
        var selectorParam = Expression.Parameter(typeof(TSource), "keySelector");
        var sourceParam = Expression.Parameter(typeof(IEnumerable<TSource>), "source");
        return 
            Expression.Lambda<Func<IEnumerable<TSource>, IOrderedEnumerable<TSource>>>
            (
                Expression.Call
                (
                    orderBy.MakeGenericMethod(typeof(TSource), pi.PropertyType), 
                    sourceParam, 
                    Expression.Lambda
                    (
                        typeof(Func<,>).MakeGenericType(typeof(TSource), pi.PropertyType), 
                        Expression.Property(selectorParam, pi), 
                        selectorParam
                    )
                ), 
                sourceParam
            )
            .Compile()(source);
    }

    public static IEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, string propertyName, bool ascending)
    {
        return ascending ? source.OrderBy(propertyName) : source.OrderBy(propertyName).Reverse();
    }

}

Eine andere, diesmal für jeden IQueryable:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public static class IQueryableHelper
{

    static MethodInfo orderBy = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderBy" && x.GetParameters().Length == 2).First();
    static MethodInfo orderByDescending = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderByDescending" && x.GetParameters().Length == 2).First();

    public static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, params string[] sortDescriptors)
    {
        return sortDescriptors.Length > 0 ? source.OrderBy(sortDescriptors, 0) : source;
    }

    static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string[] sortDescriptors, int index)
    {
        if (index < sortDescriptors.Length - 1) source = source.OrderBy(sortDescriptors, index + 1);
        string[] splitted = sortDescriptors[index].Split(' ');
        var pi = typeof(TSource).GetProperty(splitted[0], BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.IgnoreCase);
        var selectorParam = Expression.Parameter(typeof(TSource), "keySelector");
        return source.Provider.CreateQuery<TSource>(Expression.Call((splitted.Length > 1 && string.Compare(splitted[1], "desc", StringComparison.Ordinal) == 0 ? orderByDescending : orderBy).MakeGenericMethod(typeof(TSource), pi.PropertyType), source.Expression, Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(TSource), pi.PropertyType), Expression.Property(selectorParam, pi), selectorParam)));
    }

}

Sie können mehrere Sortierkriterien wie folgt übergeben:

var q = dc.Felhasznalos.OrderBy(new string[] { "Email", "FelhasznaloID desc" });
Andras Vass
quelle
4

Die von Rashack bereitgestellte Lösung funktioniert leider nicht für Werttypen (int, enums usw.).

Damit es mit jeder Art von Eigenschaft funktioniert, habe ich folgende Lösung gefunden:

public static Expression<Func<T, object>> GetLambdaExpressionFor<T>(this string sortColumn)
    {
        var type = typeof(T);
        var parameterExpression = Expression.Parameter(type, "x");
        var body = Expression.PropertyOrField(parameterExpression, sortColumn);
        var convertedBody = Expression.MakeUnary(ExpressionType.Convert, body, typeof(object));

        var expression = Expression.Lambda<Func<T, object>>(convertedBody, new[] { parameterExpression });

        return expression;
    }
Antoine Jaussoin
quelle
Das ist großartig und wird sogar richtig in SQL übersetzt!
Xavier Poinas
1

Hinzufügen zu dem, was @Samuel und @bluish getan haben. Dies ist viel kürzer, da die Aufzählung in diesem Fall nicht erforderlich war. Als zusätzlichen Bonus, wenn der Aufstieg das gewünschte Ergebnis ist, können Sie nur 2 Parameter anstelle von 3 übergeben, da true die Standardantwort auf den dritten Parameter ist.

public void Sort<TKey>(ref List<Person> list, Func<Person, TKey> sorter, bool isAscending = true)
{
    list = isAscending ? list.OrderBy(sorter) : list.OrderByDescending(sorter);
}
Stephen Whitlock
quelle
0

Wenn Sie den Namen und die Sortierrichtung der Sortierspalte als Zeichenfolge erhalten und den Schalter nicht verwenden möchten oder die Syntax \ else zur Bestimmung der Spalte verwenden möchten, ist dieses Beispiel möglicherweise für Sie interessant:

private readonly Dictionary<string, Expression<Func<IuInternetUsers, object>>> _sortColumns = 
        new Dictionary<string, Expression<Func<IuInternetUsers, object>>>()
    {
        { nameof(ContactSearchItem.Id),             c => c.Id },
        { nameof(ContactSearchItem.FirstName),      c => c.FirstName },
        { nameof(ContactSearchItem.LastName),       c => c.LastName },
        { nameof(ContactSearchItem.Organization),   c => c.Company.Company },
        { nameof(ContactSearchItem.CustomerCode),   c => c.Company.Code },
        { nameof(ContactSearchItem.Country),        c => c.CountryNavigation.Code },
        { nameof(ContactSearchItem.City),           c => c.City },
        { nameof(ContactSearchItem.ModifiedDate),   c => c.ModifiedDate },
    };

    private IQueryable<IuInternetUsers> SetUpSort(IQueryable<IuInternetUsers> contacts, string sort, string sortDir)
    {
        if (string.IsNullOrEmpty(sort))
        {
            sort = nameof(ContactSearchItem.Id);
        }

        _sortColumns.TryGetValue(sort, out var sortColumn);
        if (sortColumn == null)
        {
            sortColumn = c => c.Id;
        }

        if (string.IsNullOrEmpty(sortDir) || sortDir == SortDirections.AscendingSort)
        {
            contacts = contacts.OrderBy(sortColumn);
        }
        else
        {
            contacts = contacts.OrderByDescending(sortColumn);
        }

        return contacts;
    }

Lösung basierend auf der Verwendung eines Wörterbuchs, das die für die Sortierspalte erforderliche Verbindung über Ausdruck> und dessen Schlüsselzeichenfolge herstellt.

Online123321
quelle