Sắp xếp danh sách bằng Lambda / Linq cho các đối tượng


274

Tôi có tên của "sắp xếp theo thuộc tính" trong một chuỗi. Tôi sẽ cần sử dụng Lambda / Linq để sắp xếp danh sách các đối tượng.

Ví dụ:

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. Thay vì sử dụng một loạt các if để kiểm tra tên trường (sortBy), có cách nào tốt hơn để thực hiện việc sắp xếp
  2. Là sắp xếp nhận thức của datatype?


Tôi thấy sortBy == "FirstName" . Thay vào đó, OP có nghĩa là làm .Equals () ?
Pieter

3
@Pieter có lẽ anh ta có ý so sánh sự bình đẳng, nhưng tôi nghi ngờ anh ta "có nghĩa là làm .Equals ()". Typo thường không dẫn đến mã có chức năng.
C.Evenhuis

1
@Pieter Câu hỏi của bạn chỉ có ý nghĩa nếu bạn nghĩ có gì đó không ổn với ==... cái gì?
Jim Balter

Câu trả lời:


365

Điều này có thể được thực hiện như là

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

.NET framework đang truyền lambda (emp1,emp2)=>intthành mộtComparer<Employee>.

Điều này có lợi thế là được gõ mạnh mẽ.


Tôi thường tình cờ viết các toán tử so sánh phức tạp, liên quan đến nhiều tiêu chí so sánh và so sánh GUID không an toàn cuối cùng để đảm bảo tính không đối xứng. Bạn sẽ sử dụng một biểu thức lambda cho một so sánh phức tạp như thế? Nếu không, điều này có nghĩa là so sánh biểu thức lambda chỉ nên giới hạn trong các trường hợp đơn giản?
Simone

4
vâng tôi không thấy nó như thế này? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2) ;
Thứ hai

1
Làm thế nào để sắp xếp ngược lại?
JerryGidel

1
@JerryGidel trao đổi các thông số ... emp2.FirstName.CompareTo (emp1.FirstName), v.v.
Chris Hynes

3
Chỉ vì nó là một tham chiếu chức năng mà nó không phải là một lớp lót. Bạn chỉ có thể viếtlist.sort(functionDeclaredElsewhere)
The Hoff

74

Một điều bạn có thể làm là thay đổi Sortđể nó sử dụng lambdas tốt hơn.

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

Bây giờ bạn có thể chỉ định trường để sắp xếp khi gọi Sortphương thức.

Sort(ref employees, e => e.DOB, SortDirection.Descending);

7
Vì cột sắp xếp nằm trong một chuỗi, bạn vẫn cần một khối chuyển đổi / if-other để xác định hàm nào sẽ truyền nó.
tvanfosson

1
Bạn không thể đưa ra giả định đó. Ai biết mã của anh ta gọi nó như thế nào.
Samuel

3
Ông tuyên bố trong câu hỏi rằng "sắp xếp theo tài sản" nằm trong một chuỗi. Tôi chỉ đi theo câu hỏi của anh ấy.
tvanfosson

6
Tôi nghĩ nhiều khả năng vì nó đến từ một điều khiển sắp xếp trên một trang web chuyển cột sắp xếp trở lại dưới dạng tham số chuỗi. Dù sao đó cũng là trường hợp sử dụng của tôi.
tvanfosson

2
@tvanfosson - Bạn nói đúng, tôi có một điều khiển tùy chỉnh có thứ tự và tên trường dưới dạng chuỗi
DotnetDude

55

Bạn có thể sử dụng Reflection để có được giá trị của tài sản.

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

Trong đó TypeHelper có một phương thức tĩnh như:

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

Bạn cũng có thể muốn xem xét Dynamic LINQ từ thư viện Mẫu của VS2008 . Bạn có thể sử dụng tiện ích mở rộng IEnumerable để truyền Danh sách dưới dạng IQueryable và sau đó sử dụng tiện ích mở rộng OrderBy liên kết động.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );

1
Trong khi điều này không giải quyết được vấn đề của anh ta, chúng tôi có thể muốn tránh xa anh ta bằng cách sử dụng một chuỗi để sắp xếp nó. Câu trả lời tốt không hơn không kém.
Samuel

Bạn có thể sử dụng Dynamic linq mà không cần Linq để Sql để làm những gì anh ấy cần ... Tôi thích nó
JoshBerke

Chắc chắn rồi. Bạn có thể chuyển đổi nó thành IQueryable. Không nghĩ về điều đó. Cập nhật câu trả lời của tôi.
tvanfosson

@Samuel Nếu sắp xếp là một biến tuyến, không có cách nào khác để sắp xếp nó.
Chev

1
@ChuckD - mang bộ sưu tập vào bộ nhớ trước khi bạn cố gắng sử dụng nó, ví dụ:collection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson

20

Đây là cách tôi giải quyết vấn đề của mình:

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

16

Xây dựng thứ tự theo biểu thức có thể được đọc ở đây

Bị đánh cắp một cách đáng xấu hổ từ trang trong liên kết:

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

Có vấn đề liên quan đến điều này: Sắp xếp DateTime.
CrazyEnigma

Ngoài ra làm thế nào về các lớp tổng hợp, tức là Person.Employer.CompanyName?
davewilliams459

Tôi về cơ bản đã làm điều tương tự và câu trả lời này đã giải quyết nó.
Jason.Net

8

Bạn có thể sử dụng sự phản chiếu để truy cập vào tài sản.

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

Ghi chú

  1. Tại sao bạn vượt qua danh sách bằng cách tham khảo?
  2. Bạn nên sử dụng một enum cho hướng sắp xếp.
  3. Bạn có thể nhận được một giải pháp sạch hơn nhiều nếu bạn chuyển một biểu thức lambda chỉ định thuộc tính để sắp xếp thay vì tên thuộc tính dưới dạng chuỗi.
  4. Trong danh sách ví dụ của tôi == null sẽ gây ra NullReferenceException, bạn nên nắm bắt trường hợp này.

Có ai khác nhận thấy rằng đây là một kiểu trả về void nhưng trả về danh sách?
emd

Ít nhất không ai quan tâm để sửa nó và tôi đã không chú ý đến nó bởi vì tôi đã không viết mã bằng IDE. Cảm ơn đã chỉ ra rằng.
Daniel Brückner

6

Sắp xếp sử dụng giao diện IComparable, nếu loại thực hiện nó. Và bạn có thể tránh các if bằng cách triển khai một IComparer tùy chỉnh:

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

và sau đó

list.Sort(new EmpComp(sortBy));

FYI: Sắp xếp là một phương thức của Danh sách <T> và không phải là phần mở rộng Linq.
Serguei

5

Trả lời cho 1.:

Bạn có thể tự xây dựng một cây biểu thức có thể được truyền vào OrderBy bằng cách sử dụng tên dưới dạng chuỗi. Hoặc bạn có thể sử dụng sự phản chiếu như được đề xuất trong một câu trả lời khác, có thể là công việc ít hơn.

Chỉnh sửa : Đây là một ví dụ hoạt động về xây dựng cây biểu thức bằng tay. (Sắp xếp trên X. Giá trị, khi chỉ biết tên "Giá trị" của tài sản). Bạn có thể (nên) xây dựng một phương pháp chung để thực hiện nó.

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

Tuy nhiên, việc xây dựng một cây biểu thức đòi hỏi bạn phải biết các kiểu phân từ. Điều đó có thể hoặc không thể là một vấn đề trong kịch bản sử dụng của bạn. Nếu bạn không biết bạn nên sắp xếp loại nào, việc sử dụng sự phản chiếu sẽ dễ dàng hơn.

Trả lời cho 2.:

Có, vì So sánh <T> .Default sẽ được sử dụng để so sánh, nếu bạn không xác định rõ ràng so sánh.


Bạn có một ví dụ về việc xây dựng một cây biểu thức được truyền vào OrderBy không?
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();
    }

}

Một số khác, lần này cho bất kỳ IQueryable nào:

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

}

Bạn có thể vượt qua nhiều tiêu chí sắp xếp, như thế này:

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

4

Giải pháp được cung cấp bởi Rashack không hoạt động đối với các loại giá trị (int, enums, v.v.) không may.

Để nó hoạt động với bất kỳ loại tài sản nào, đây là giải pháp tôi tìm thấy:

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

Điều này thật tuyệt vời và thậm chí còn được dịch sang SQL!
Xavier Poinas

1

Thêm vào những gì @Samuel và @bluish đã làm. Điều này ngắn hơn nhiều vì Enum là không cần thiết trong trường hợp này. Cộng với phần thưởng bổ sung khi tăng dần là kết quả mong muốn, bạn chỉ có thể truyền 2 tham số thay vì 3 vì true là câu trả lời mặc định cho tham số thứ ba.

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

0

Nếu bạn nhận được tên cột sắp xếp và hướng sắp xếp dưới dạng chuỗi và không muốn sử dụng chuyển đổi hoặc cú pháp if \ other để xác định cột, thì ví dụ này có thể thú vị đối với bạn:

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

Giải pháp dựa trên việc sử dụng Từ điển kết nối cần thiết cho sắp xếp cột thông qua Biểu thức> và chuỗi khóa của nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.