เรียงลำดับรายการโดยใช้ Lambda / Linq กับวัตถุ


276

ฉันมีชื่อของ "เรียงลำดับตามคุณสมบัติ" ในสตริง ฉันจะต้องใช้ Lambda / Linq เพื่อเรียงลำดับรายการของวัตถุ

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. แทนที่จะใช้พวงของ ifs เพื่อตรวจสอบ fieldname (sortBy) มีวิธีการเรียงลำดับที่สะอาดกว่า
  2. มีการเรียงลำดับของชนิดข้อมูลหรือไม่


ฉันเห็นSortBy == "FirstName" OP หมายถึงทำ. Qual ()แทนหรือไม่
Pieter

3
@ ปีเตอร์เขาอาจหมายถึงการเปรียบเทียบความเท่าเทียมกัน แต่ฉันสงสัยว่าเขา "หมายถึงการทำ. Equals ()" Typo มักจะไม่ส่งผลในรหัสที่ทำงาน
C.Evenhuis

1
@ ปีเตอร์คำถามของคุณจะสมเหตุสมผลถ้าคุณคิดว่ามีบางอย่างผิดปกติกับ==... อะไรนะ?
Jim Balter

คำตอบ:


367

ซึ่งสามารถทำได้ดังนี้

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

. NET Framework กำลังส่งแลมบ์ดา(emp1,emp2)=>intเป็นComparer<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 สลับ params ... emp2.FirstName.CompareTo (emp1.FirstName) ฯลฯ
Chris Hynes

3
เพียงเพราะมันเป็นการอ้างอิงฟังก์ชั่นไม่จำเป็นต้องเป็นสายการบินเดียว คุณสามารถเขียนได้list.sort(functionDeclaredElsewhere)
The Hoff

74

สิ่งหนึ่งที่คุณสามารถทำได้คือเปลี่ยนSortมันใช้ประโยชน์จาก 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);
}

ตอนนี้คุณสามารถระบุเขตข้อมูลที่จะเรียงเมื่อเรียกใช้Sortเมธอด

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

7
เนื่องจากคอลัมน์การเรียงลำดับอยู่ในสตริงคุณยังต้องมีสวิตช์ / 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 );
    }
}

นอกจากนี้คุณยังอาจต้องการที่จะมองแบบไดนามิก LINQ จากห้องสมุด VS2008 ตัวอย่าง คุณสามารถใช้ส่วนขยาย IEnumerable เพื่อส่งรายการเป็น IQueryable แล้วใช้ส่วนขยายแบบไดนามิกลิงก์ OrderBy

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

1
ในขณะที่วิธีนี้แก้ปัญหาของเขาได้เราอาจต้องการคัดเขาออกจากการใช้สตริงเพื่อจัดเรียง คำตอบที่ดีไม่มีเลย
ซามูเอล

คุณสามารถใช้แบบไดนามิก LINQ โดยไม่ต้อง LINQ กับ SQL ที่จะทำสิ่งที่เขาต้องการ ... ฉันรักมัน
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
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. คุณควรใช้ enum สำหรับทิศทางการเรียงลำดับ
  3. คุณสามารถหาโซลูชันที่สะอาดกว่านี้ได้ถ้าคุณส่งนิพจน์แลมบ์ดาที่ระบุคุณสมบัติเพื่อจัดเรียงแทนชื่อคุณสมบัติเป็นสตริง
  4. ในรายการตัวอย่างของฉัน == null จะทำให้ NullReferenceException คุณควรตรวจสอบกรณีนี้

มีใครเคยสังเกตเห็นว่านี่เป็นโมฆะประเภทส่งคืน แต่กลับรายการ?
emd

อย่างน้อยก็ไม่มีใครสนใจที่จะแก้ไขและฉันไม่ได้สังเกตเพราะฉันไม่ได้เขียนรหัสโดยใช้ IDE ขอบคุณสำหรับการชี้ให้เห็นว่า
Daniel Brückner

6

เรียงใช้อินเทอร์เฟซ IComparable ถ้าชนิดที่ใช้นั้น และคุณสามารถหลีกเลี่ยง ifs ได้โดยการนำ IComparer ที่กำหนดเองไปใช้:

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

FYI: Sort เป็นวิธีการของรายการ <T> และไม่ใช่ส่วนขยาย Linq
Serguei

5

คำตอบสำหรับ 1 .:

คุณควรจะสามารถสร้างทรีนิพจน์ด้วยตนเองที่สามารถส่งผ่านไปยัง OrderBy โดยใช้ชื่อเป็นสตริง หรือคุณอาจใช้การไตร่ตรองตามที่แนะนำในคำตอบอื่นซึ่งอาจทำงานได้น้อยลง

แก้ไข : นี่เป็นตัวอย่างการทำงานของการสร้างทรีนิพจน์ด้วยตนเอง (เรียงลำดับตาม 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 .:

ใช่เนื่องจาก Comparer <T> ค่าเริ่มต้นจะถูกใช้สำหรับการเปรียบเทียบหากคุณไม่ได้กำหนดตัวเปรียบเทียบอย่างชัดเจน


คุณมีตัวอย่างของการสร้างแผนภูมินิพจน์ที่จะส่งผ่านไปยัง 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();
    }

}

อีกหนึ่งเวลานี้สำหรับ 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 อย่างถูกต้องด้วย!
ซาเวียร์ Poinas

1

การเพิ่มสิ่งที่ @Samuel และ @bluish ทำ ซึ่งสั้นกว่ามากเนื่องจาก Enum ไม่จำเป็นในกรณีนี้ นอกจากนี้ยังเป็นโบนัสเพิ่มเติมเมื่อ Ascending เป็นผลลัพธ์ที่ต้องการคุณสามารถส่งได้เพียง 2 พารามิเตอร์แทน 3 เนื่องจาก true เป็นคำตอบเริ่มต้นสำหรับพารามิเตอร์ที่สาม

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

0

หากคุณได้รับชื่อคอลัมน์เรียงลำดับและทิศทางการจัดเรียงเป็นสตริงและไม่ต้องการใช้สวิตช์หรือถ้า \ 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> และสตริงคีย์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.