Menyortir daftar menggunakan Lambda / Linq ke objek

276

Saya memiliki nama "urutkan berdasarkan properti" dalam sebuah string. Saya perlu menggunakan Lambda / Linq untuk mengurutkan daftar objek.

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. Alih-alih menggunakan sekelompok ifs untuk memeriksa fieldname (sortBy), apakah ada cara yang lebih bersih untuk melakukan penyortiran
  2. Apakah semacam menyadari tipe data?
DotnetDude
sumber
3
Dupe: stackoverflow.com/questions/606997/…
Mehrdad Afshari
Saya melihat sortBy == "FirstName" . Apakah OP bermaksud melakukan .Equals () sebagai gantinya?
Pieter
3
@Pieter dia mungkin memang bermaksud membandingkan kesetaraan, tapi saya ragu dia "bermaksud melakukan. Setara ()". Typo biasanya tidak menghasilkan kode yang berfungsi.
C.Evenhuis
1
@Pieter Pertanyaan Anda hanya masuk akal jika Anda berpikir ada sesuatu yang salah dengan ==... apa?
Jim Balter

Jawaban:

367

Ini dapat dilakukan sebagai

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

Kerangka .NET melempar lambda (emp1,emp2)=>intsebagaiComparer<Employee>.

Ini memiliki keuntungan karena sangat diketik.

gls123
sumber
Sering terjadi pada saya untuk menulis operator perbandingan yang kompleks, melibatkan beberapa kriteria perbandingan dan perbandingan GUID yang gagal pada akhirnya untuk memastikan antisimetri. Apakah Anda menggunakan ekspresi lambda untuk perbandingan yang kompleks seperti itu? Jika tidak, apakah ini berarti perbandingan ekspresi lambda hanya terbatas pada kasus sederhana?
Simone
4
ya saya tidak melihatnya seperti ini? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null)))) ;
Sabtu
1
bagaimana cara mengurutkan secara terbalik?
JerryGoyal
1
@JerryGoyal menukar params ... emp2.FirstName.CompareTo (emp1.FirstName) dll.
Chris Hynes
3
Hanya karena ini adalah referensi fungsi tidak harus menjadi satu liner. Anda bisa menulislist.sort(functionDeclaredElsewhere)
The Hoff
74

Satu hal yang bisa Anda lakukan adalah mengubah Sortsehingga lebih baik menggunakan lambdas.

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

Sekarang Anda dapat menentukan bidang yang akan disortir saat memanggil Sortmetode.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Samuel
sumber
7
Karena kolom sortir dalam string, Anda masih memerlukan sakelar / jika-blok lain untuk menentukan fungsi mana yang melewatinya.
tvanfosson
1
Anda tidak dapat membuat asumsi itu. Siapa yang tahu bagaimana kodenya menyebutnya.
Samuel
3
Dia menyatakan dalam pertanyaan bahwa "urutkan berdasarkan properti" adalah dalam sebuah string. Saya hanya menjawab pertanyaannya.
tvanfosson
6
Saya pikir itu lebih mungkin karena itu berasal dari kontrol semacam pada halaman web yang melewati kolom sortir kembali sebagai parameter string. Itu akan menjadi kasus penggunaan saya.
tvanfosson
2
@tvanfosson - Anda benar, saya punya kontrol khusus yang memiliki urutan dan nama bidang sebagai string
DotnetDude
55

Anda bisa menggunakan Refleksi untuk mendapatkan nilai properti.

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

Di mana TypeHelper memiliki metode statis seperti:

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

Anda mungkin juga ingin melihat Dynamic LINQ dari perpustakaan VS2008 Samples . Anda bisa menggunakan ekstensi IEnumerable untuk melemparkan Daftar sebagai IQueryable dan kemudian menggunakan ekstensi tautan dinamis OrderBy.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
tvanfosson
sumber
1
Meskipun ini menyelesaikan masalahnya, kami mungkin ingin menjauhkannya dari menggunakan string untuk mengatasinya. Jawaban yang bagus.
Samuel
Anda dapat menggunakan Linq dinamis tanpa Linq ke Sql untuk melakukan apa yang dia butuhkan ... Saya menyukainya
JoshBerke
Tentu. Anda dapat mengubahnya menjadi IQueryable. Tidak memikirkan itu. Memperbarui jawaban saya.
tvanfosson
@Samuel Jika pengurutan tersebut masuk sebagai variabel rute, tidak ada cara lain untuk mengurutkannya.
Chev
1
@ChuckD - bawa koleksi ke dalam memori sebelum Anda mencoba menggunakannya, misalnyacollection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson
20

Inilah cara saya memecahkan masalah saya:

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
sumber
16

Membangun urutan dengan ekspresi dapat dibaca di sini

Tanpa malu-malu dicuri dari halaman di tautan:

// 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
sumber
Ada masalah yang terkait dengan ini: Urutkan DateTime.
CrazyEnigma
Juga bagaimana dengan kelas komposit, yaitu Person.Employer.CompanyName?
davewilliams459
Saya pada dasarnya melakukan hal yang sama dan jawaban ini menyelesaikannya.
Jason.Net
8

Anda dapat menggunakan refleksi untuk mengakses properti.

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

Catatan

  1. Mengapa Anda melewati daftar dengan referensi?
  2. Anda harus menggunakan enum untuk arah sortir.
  3. Anda bisa mendapatkan solusi yang jauh lebih bersih jika Anda akan melewatkan ekspresi lambda yang menentukan properti untuk diurutkan dengan alih-alih nama properti sebagai string.
  4. Dalam daftar contoh saya == null akan menyebabkan NullReferenceException, Anda harus mengetahui kasus ini.
Daniel Brückner
sumber
Adakah orang lain yang pernah memperhatikan bahwa ini adalah tipe pengembalian batal tetapi daftar pengembalian?
emd
Setidaknya tidak ada yang peduli untuk memperbaikinya dan saya tidak menyadarinya karena saya tidak menulis kode menggunakan IDE. Terima kasih telah menunjukkannya.
Daniel Brückner
6

Sortir menggunakan antarmuka IComparable, jika tipe mengimplementasikannya. Dan Anda dapat menghindari ifs dengan menerapkan IComparer khusus:

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

lalu

list.Sort(new EmpComp(sortBy));
Serguei
sumber
FYI: Sortir adalah metode Daftar <T> dan bukan ekstensi Linq.
Serguei
5

Jawaban untuk 1 .:

Anda harus dapat secara manual membangun pohon ekspresi yang dapat diteruskan ke OrderBy menggunakan nama sebagai string. Atau Anda dapat menggunakan refleksi seperti yang disarankan dalam jawaban lain, yang mungkin kurang berhasil.

Sunting : Ini adalah contoh kerja membangun pohon ekspresi secara manual. (Mengurutkan pada X.Value, ketika hanya mengetahui nama "Nilai" dari properti). Anda dapat (harus) membangun metode generik untuk melakukannya.

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

Namun, membangun pohon ekspresi mengharuskan Anda mengetahui jenis-jenis partikulat. Itu mungkin atau mungkin tidak menjadi masalah dalam skenario penggunaan Anda. Jika Anda tidak tahu jenis apa yang harus Anda sortir, mungkin akan lebih mudah menggunakan refleksi.

Jawaban untuk 2 .:

Ya, karena Pembanding <T>. Kesalahan akan digunakan untuk perbandingan, jika Anda tidak secara eksplisit mendefinisikan pembanding.

driis
sumber
Apakah Anda memiliki contoh membangun pohon ekspresi untuk diteruskan ke OrderBy?
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();
    }

}

Satu lagi, kali ini untuk setiap 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)));
    }

}

Anda dapat melewati beberapa kriteria sortir, seperti ini:

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

Sayangnya, solusi yang disediakan oleh Rashack tidak berfungsi untuk tipe nilai (int, enum, dll.).

Agar dapat bekerja dengan semua jenis properti, ini adalah solusi yang saya temukan:

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
sumber
Ini luar biasa dan bahkan diterjemahkan ke SQL!
Xavier Poinas
1

Menambahkan apa yang @Samuel dan @bluish lakukan. Ini jauh lebih pendek karena Enum tidak perlu dalam kasus ini. Ditambah sebagai bonus tambahan ketika Ascending adalah hasil yang diinginkan, Anda hanya dapat memberikan 2 parameter daripada 3 karena true adalah jawaban default untuk parameter ketiga.

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
sumber
0

Jika Anda mendapatkan urutkan nama kolom dan urutkan arah sebagai string dan tidak ingin menggunakan sakelar atau jika \ sintaks lain untuk menentukan kolom, maka contoh ini mungkin menarik bagi Anda:

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

Solusi berdasarkan penggunaan Kamus yang menghubungkan diperlukan untuk mengurutkan kolom melalui Expression> dan string utamanya.

Online123321
sumber