Lambda / Linq를 사용하여 객체에 대한 목록 정렬


275

문자열에 "속성 정렬"의 이름이 있습니다. Lambda / Linq를 사용하여 객체 목록을 정렬해야합니다.

전의:

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. 필드 이름 (sortBy)을 확인하기 위해 많은 if를 사용하는 대신 정렬을 수행하는 더 확실한 방법이 있습니까?
  2. 정렬이 데이터 유형을 알고 있습니까?


sortBy == "FirstName"이 보입니다 . OP가 대신 .Equals () 를 수행 했습니까 ?
Pieter

3
@Pieter 그는 평등을 비교하는 것을 의미했지만 아마도 ".Equals ()"를 할 의구심을 가지고 있습니다. 오타는 일반적으로 작동하는 코드를 생성하지 않습니다.
C.Evenhuis

1
@Pieter 당신의 질문은 당신이 뭔가 잘못되었다고 생각하는 경우에만 의미가 있습니다 ==... 무엇?
Jim Balter

답변:


366

이것은 다음과 같이 수행 할 수 있습니다

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

.NET 프레임 워크는 람다 (emp1,emp2)=>intComparer<Employee>.

이것은 강력하게 타이핑되는 장점이 있습니다.


비대칭 성을 보장하기 위해 여러 비교 기준과 최종 안전 GUID 비교를 포함하는 복잡한 비교 연산자를 작성하는 것이 종종 발생했습니다. 복잡한 비교를 위해 람다 식을 사용 하시겠습니까? 그렇지 않다면, 이것은 람다 식 비교가 단순한 경우에만 제한되어야한다는 것을 의미합니까?
Simone

4
그래, 나는 이것과 같은 것을 보지 못합니까? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null))) ;

1
반대로 정렬하는 방법?
JerryGoyal

1
@JerryGoyal는 매개 변수를 교환 ... emp2.FirstName.CompareTo (emp1.FirstName) 등
크리스 하이 네스

3
함수 참조이기 때문에 하나의 라이너 일 필요는 없습니다. 당신은 그냥 쓸 수 있습니다list.sort(functionDeclaredElsewhere)
Hoff

74

당신이 할 수있는 한 가지는 Sort람다를 더 잘 사용하도록 변경 하는 것입니다.

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

이제 Sort메소드를 호출 할 때 정렬 할 필드를 지정할 수 있습니다 .

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

7
정렬 열이 문자열에 있기 때문에 전달할 함수를 결정하려면 여전히 switch / if-else 블록이 필요합니다.
tvanfosson

1
당신은 그 가정을 할 수 없습니다. 그의 코드가 어떻게 그것을 호출하는지 아는 사람.
사무엘

3
그는 질문에서 "속성 정렬"이 문자열로되어 있다고 언급했다. 나는 단지 그의 질문으로 가고 있습니다.
tvanfosson

6
정렬 열을 문자열 매개 변수로 다시 전달하는 웹 페이지의 정렬 컨트롤에서 왔기 때문에 더 가능성이 있다고 생각합니다. 어쨌든 내 유스 케이스가 될 것입니다.
tvanfosson

2
@tvanfosson-당신 말이 맞아요, 순서와 필드 이름을 문자열로 한 커스텀 컨트롤이 있습니다
DotnetDude

55

Reflection을 사용하여 속성 값을 얻을 수 있습니다.

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

TypeHelper에는 다음과 같은 정적 메소드가 있습니다.

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

VS2008 샘플 라이브러리 에서 Dynamic LINQ를 볼 수도 있습니다 . IEnumerable 확장을 사용하여 List를 IQueryable로 캐스팅 한 다음 Dynamic link OrderBy 확장을 사용할 수 있습니다.

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

1
이것이 그의 문제를 해결하는 동안, 우리는 문자열을 사용하여 정렬하는 것을 피할 수 있습니다. 그럼에도 불구하고 좋은 답변.
사무엘

당신은 내가 사랑 ... 그가 필요한 것을 할 LINQ to SQL은하지 않고 동적 LINQ를 사용할 수 있습니다
JoshBerke

확실한. IQueryable로 변환 할 수 있습니다. 그것에 대해 생각하지 않았다. 내 답변을 업데이트합니다.
tvanfosson

@Samuel 정렬이 경로 변수로 들어오는 경우 다른 정렬 방법이 없습니다.
Chev

1
@ChuckD-컬렉션을 사용하기 전에 메모리에 컬렉션을 가져옵니다. 예 :collection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson

20

이것이 내 문제를 해결 한 방법입니다.

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

식으로 주문 작성은 여기에서 읽을 수 있습니다

링크의 페이지에서 뻔뻔스럽게 도난당했습니다.

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

이것과 관련된 문제가 있습니다 : DateTime sort.
CrazyEnigma

또한 Person.Employer.CompanyName과 같은 복합 클래스는 어떻습니까?
davewilliams459

나는 본질적으로 같은 일을하고 있었고이 대답은 그것을 해결했습니다.
Jason.Net

8

리플렉션을 사용하여 속성에 액세스 할 수 있습니다.

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

노트

  1. 왜 목록을 참조로 전달합니까?
  2. 정렬 방향으로 열거 형을 사용해야합니다.
  3. 속성 이름 대신 문자열로 정렬 할 속성을 지정하는 람다 식을 전달하면 훨씬 깨끗한 솔루션을 얻을 수 있습니다.
  4. 내 예제 list == null에서 NullReferenceException이 발생 하므로이 경우를 포착해야합니다.

다른 사람이 이것이 반환 유형 void이지만 목록을 반환한다는 것을 알고 있습니까?
emd

적어도 아무도 그것을 고칠 필요가 없었고 IDE를 사용하여 코드를 작성하지 않았기 때문에 그것을 알지 못했습니다. 지적 해 주셔서 감사합니다.
Daniel Brückner 2016 년

6

유형이 구현하는 경우 Sort는 IComparable 인터페이스를 사용합니다. 또한 사용자 정의 IComparer를 구현하여 if를 피할 수 있습니다.

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

그리고

list.Sort(new EmpComp(sortBy));

참고 : 정렬은 List <T>의 방법이며 Linq 확장이 아닙니다.
Serguei

5

1에 대한 답변 ::

이름을 문자열로 사용하여 OrderBy에 전달할 수있는 표현식 트리를 수동으로 빌드 할 수 있어야합니다. 또는 다른 답변에서 제안한 것처럼 반사를 사용하면 작업이 적을 수 있습니다.

편집 : 식 트리를 수동으로 작성하는 실제 예입니다. (속성의 이름 "Value"만 알고있을 때 X.Value에 정렬). 일반적인 방법을 만들 수 있습니다.

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

그러나 표현식 트리를 작성하려면 참여 유형을 알아야합니다. 사용 시나리오에서 문제가 될 수도 있고 아닐 수도 있습니다. 어떤 유형을 정렬해야하는지 모른다면 리플렉션을 사용하는 것이 더 쉬울 것입니다.

2에 대한 답변 ::

예. 비교자를 명시 적으로 정의하지 않으면 비교 자 <T> .Default가 비교에 사용됩니다.


OrderBy에 전달할 표현식 트리를 작성하는 예가 있습니까?
DotnetDude 2009

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

}

이번에는 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)));
    }

}

다음과 같이 여러 정렬 기준을 전달할 수 있습니다.

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

4

Rashack이 제공하는 솔루션은 불행히도 값 유형 (int, enums 등)에는 작동하지 않습니다.

모든 유형의 속성에서 작동하려면 이것이 내가 찾은 해결책입니다.

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

이것은 훌륭하고 SQL로 올바르게 번역됩니다!
Xavier Poinas

1

@Samuel과 @bluish가 한 일에 추가. 이 경우 Enum이 필요하지 않으므로 훨씬 짧습니다. 또한 오름차순이 원하는 결과 일 때 추가 보너스로 true가 세 번째 매개 변수에 대한 기본 응답이므로 3 대신 3 개의 매개 변수 만 전달할 수 있습니다.

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

0

정렬 열 이름과 정렬 방향을 문자열로 가져 와서 switch 또는 if \ else 구문을 사용하여 열을 결정하지 않으려면이 예제가 흥미로울 수 있습니다.

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

Expression> 및 해당 키 문자열을 통해 정렬 열에 필요한 Dictionary를 사용하는 솔루션.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.