如何将 LINQ 的 OrderBy 方向指定为布尔值?

How do I specify LINQ's OrderBy direction as a boolean?

提问人:James 提问时间:8/3/2012 更新时间:8/3/2012 访问量:7314

问:

我有一个简单的数据类,它有这个签名:

internal interface IMyClass {
    string Letter { get; }
    int Number { get; }
}

我希望能够根据字段(指定为 )和方向(指定为string sortFieldbool isAscending)

目前我正在使用 (每种情况下的升序逻辑为switchif)

IEnumerable<IMyClass> lst = new IMyClass[];//provided as paramater
switch (sortField)
{
    case "letter":
        if( isAscending ) {
            lst = lst.OrderBy( s => s.Letter );
        } else {
            lst = lst.OrderByDescending( s => s.Letter );
        }
        break;
    case "number":
        if( isAscending ) {
            lst = lst.OrderBy( s => s.Number );
        } else {
            lst = lst.OrderByDescending( s => s.Number );
        }
        break;
}

对于 2 个属性来说,这是非常丑陋的,但是当排序逻辑不同时,它就会成为一个问题(我们还看到在代码中重复了两次)s => s.Number

问题传递布尔值以选择排序方向的最佳方式是什么?

我试过什么我已经拆解了System.Core.dll并找到了OrderBy Extension方法实现:

订购方式:

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector
    ){

    return new OrderedEnumerable<TSource, TKey>(
        source, 
        keySelector, 
        null, 
        false
    );
}

OrderByDescending:

public static IOrderedEnumerable<TSource> OrderByDescending<TSource, TKey>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector
    ){
        return new OrderedEnumerable<TSource, TKey>(
            source, 
            keySelector, 
            null, 
            true
        );
}

似乎拥有 2 个命名方法的目的是将这个布尔值抽象出来。我无法像 System.Core 内部那样轻松创建自己的扩展,并且编写一个从 bool -> methodName -> bool 的层对我来说似乎是错误的。OrderedEnumberable

C# LINQ

评论


答:

20赞 Dan Tao 8/3/2012 #1

我会说写你自己的扩展方法:

public static IEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector, bool ascending)
{
    if (ascending)
    {
        return source.OrderBy(selector);
    }
    else
    {
        return source.OrderByDescending(selector);
    }
}

然后你可以写:

lst = lst.Order( s => s.Letter, isAscending );

至于指定方法名称:我希望这不会成为答案,但我认为您应该坚持使用选择器函数而不是传入字符串。走字符串路线并不能真正节省你任何输入或提高清晰度(真的比?)更快或更清晰,只会让你的代码更胖(你要么需要维护从字符串到选择器函数的某种映射,要么需要编写自定义解析逻辑来在它们之间进行转换),而且可能更脆弱(如果你走后一条路, 错误的可能性非常高)。"letter"s => s.Letter

如果你打算从用户输入中获取一个字符串来自定义排序,当然,你别无选择,所以请随意忽略我令人沮丧的评论!


编辑:由于您正在接受用户输入,因此这就是我所说的映射的意思:

class CustomSorter
{
    static Dictionary<string, Func<IMyClass, object>> Selectors;

    static CustomSorter()
    {
        Selectors = new Dictionary<string, Func<IMyClass, object>>
        {
            { "letter", new Func<IMyClass, object>(x => x.Letter) },
            { "number", new Func<IMyClass, object>(x => x.Number) }
        };
    }

    public void Sort(IEnumerable<IMyClass> list, string sortField, bool isAscending)
    {
        Func<IMyClass, object> selector;
        if (!Selectors.TryGetValue(sortField, out selector))
        {
            throw new ArgumentException(string.Format("'{0}' is not a valid sort field.", sortField));
        }

        // Using extension method defined above.
        return list.Order(selector, isAscending);
    }
}

以上显然不如从字符串动态生成表达式并调用它们那么聪明;这可以被认为是一种优势或劣势,这取决于你的偏好以及你所属的团队和文化。在这种特殊情况下,我想我会投票支持手动映射,因为动态表达式路由感觉过度设计了。

评论

0赞 James 8/3/2012
要解决您的编辑问题,请执行以下操作: 之所以是字符串字段,是因为它来自用户输入(经过清理)
0赞 Dan Tao 8/3/2012
@James:哈,是这样想的(注意在你留下评论之前对我的问题的最后一次编辑!好吧,就其价值而言,我建议在这种情况下采用映射路线,而不是将字符串解析为表达式。但这是你的决定。
0赞 CraftyFella 8/3/2012 #2

我会看一下 ScottGu 在这里描述的动态 linq 选项

3赞 M. Mennan Kara 8/3/2012 #3

最好返回 IOrderedEnumerable,以防想要在末尾添加其他方法。这样,编译器会将整个链编译为一个表达式。

public static class OrderByWithBooleanExtension
{
    public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, bool isAscending)
    {
        return isAscending ? source.OrderBy(keySelector) : source.OrderByDescending(keySelector);
    }
}
0赞 Tim S. 8/3/2012 #4

您可以创建一个选择正确操作的操作:Func

var orderBy = isAscending ? (Func<Func<IMyClass, object>, IOrderedEnumerable<IMyClass>>)lst.OrderBy : lst.OrderByDescending;
switch (sortField)
{
    case "letter":
        lst = orderBy(s => s.Letter);
        break;
    case "number":
        lst = orderBy(s => s.Number);
        break;
}

结合 CraftyFella 建议的动态 LINQ,它可能看起来像:

var orderBy = isAscending ? (Func<Func<IMyClass, object>, IOrderedEnumerable<IMyClass>>)lst.OrderBy : lst.OrderByDescending;
lst = orderBy(mySortCriteria);

或者,如果您愿意,可以写一行长行:

lst = (isAscending ? (Func<Func<IMyClass, object>, IOrderedEnumerable<IMyClass>>)lst.OrderBy : lst.OrderByDescending)(mySortCriteria);

我想我更喜欢 Dan Tao 的解决方案而不是我的解决方案,只是想我会把它扔在那里,以防你觉得它有用。

1赞 Pablo Romeo 8/3/2012 #5

您可以实现自己的扩展,该扩展将按字符串属性排序,并支持通过布尔值升序和降序,例如:

public static IOrderedQueryable<T> OrderByProperty<T>(this IQueryable<T> query, string memberName, bool ascending = true)
{
    var typeParams = new[] { Expression.Parameter(typeof(T), "") };

    var pi = typeof(T).GetProperty(memberName);
    string operation = ascending ? "OrderBy" : "OrderByDescending";
    return (IOrderedQueryable<T>)query.Provider.CreateQuery(
        Expression.Call(
            typeof(Queryable),
            operation,
            new[] { typeof(T), pi.PropertyType },
            query.Expression,
            Expression.Lambda(Expression.Property(typeParams[0], pi), typeParams))
    );
}