【问题标题】:LINQ - Writing an extension method to get the row with maximum value for each groupLINQ - 编写扩展方法以获取每个组的最大值行
【发布时间】:2011-09-14 05:20:11
【问题描述】:

我的应用程序经常需要对表进行分组,然后返回该组的最大值所在的行。这在 LINQ 中很容易做到:

myTable.GroupBy(r => r.FieldToGroupBy)
.Select(r => r.Max(s => s.FieldToMaximize))
.Join(
    myTable,
    r => r,
    r => r.FieldToMaximize,
    (o, i) => i)

现在假设我想将其抽象为它自己的方法。我试着写这个:

public static IQueryable<TSource>
SelectMax<TSource, TGroupKey, TMaxKey>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, TGroupKey>> groupKeySelector,
    Expression<Func<TSource, TMaxKey>> maxKeySelector)
    where TMaxKey : IComparable
{
    return source
        .GroupBy(groupKeySelector)
        .Join(
            source,
            g => g.Max(maxKeySelector),
            r => maxKeySelector(r),
                (o, i) => i);
}

不幸的是,这不会编译:maxKeySelector 是一个表达式(所以你不能在 r 上调用它,你甚至不能将它传递给 Max。所以我尝试重写,使 maxKeySelector 成为一个函数而不是一个表达式:

public static IQueryable<TSource>
SelectMax<TSource, TGroupKey, TMaxKey>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, TGroupKey>> groupKeySelector,
    Func<TSource, TMaxKey> maxKeySelector)
    where TMaxKey : IComparable
{
    return source
        .GroupBy(groupKeySelector)
        .Join(
            source,
            g => g.Max(maxKeySelector),
            r => maxKeySelector(r),
                (o, i) => i);
}

现在编译。但它在运行时失败:“用于查询运算符'Max'的不支持的重载。”这就是我所坚持的:我需要找到将 maxKeySelector 传递给 Max() 的正确方法。

有什么建议吗?我正在使用 LINQ to SQL,这似乎有所作为。

【问题讨论】:

    标签: c# linq linq-to-sql


    【解决方案1】:

    首先,我想指出,您在 LINQ 中尝试做的事情甚至比您想象的要容易

    myTable.GroupBy(r => r.FieldToGroupBy)
        .Select(g => g.OrderByDescending(r => r.FieldToMaximize).FirstOrDefault())
    

    ...第二部分应该会让我们的生活更轻松一些:

    public static IQueryable<TSource>
    SelectMax<TSource, TGroupKey, TMaxKey>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TGroupKey>> groupKeySelector,
        Expression<Func<TSource, TMaxKey>> maxKeySelector)
        where TMaxKey : IComparable
    {
        return source
            .GroupBy(groupKeySelector)
            .Select(g => g.AsQueryable().OrderBy(maxKeySelector).FirstOrDefault());
    }
    

    关键是通过使您的组成为IQueryable,您打开了一组新的 LINQ 方法,可以采用实际表达式而不是采用 Funcs。这应该与大多数标准 LINQ 提供程序兼容。

    【讨论】:

    • 嗯,这肯定更简洁,但不幸的是它不起作用。我得到与以前几乎相同的错误:“查询运算符'OrderBy'使用不支持的重载”。
    • @ctkrohn:您使用的是什么 LINQ 提供程序?它在 LINQ to Entities.... 和 LINQ to Objects 上对我来说工作得很好。
    • @ctkrohn:好的,我在问题中添加了一个 linq-to-sql 标签。看起来 LINQ to SQL 不支持整个 AsQueryable 东西。您将必须构造一个调用 OrderBy 和 FirstOrDefault 方法的 lambda 表达式树,并将该表达式传递给 Select 方法。这是可行的,但它需要的代码比我愿意为你写的要多。对不起。
    • @ctkrohn:基于这个答案,我认为你应该能够交换 orderby 和 groupBy 命令并且仍然得到相同的结果(因为 groupby 不应该失去排序)并且应该编译并使用 Linq To Sql 运行。因此以下应该起作用 return source .OrderBy(maxKeySelector) .GroupBy(groupKeySelector) .Select(g => g.FirstOrDefault());
    • @sgmoore:这似乎也不起作用。 OrderBy 返回一个 IOrderedEnumerable,因此您不能直接在结果上调用 GroupBy。如果你通过 IQueryable 传递它,它会在运行时以某种方式引发 StackOverflowException。
    【解决方案2】:

    非常有趣。有时,“动态”在纯粹的开发和运行时执行方面的成本可能会超过其价值(恕我直言)。尽管如此,这是最简单的:

    public static IQueryable<Item> _GroupMaxs(this IQueryable<Item> list)
    {
        return list.GroupBy(x => x.Family)
            .Select(g => g.OrderByDescending(x => x.Value).First());
    }
    

    而且,这是最动态的方法:

    public static IQueryable<T> _GroupMaxs<T, TGroupCol, TValueCol>
        (this IQueryable<T> list, string groupColName, string valueColName)
    {
        // (x => x.groupColName)
        var _GroupByPropInfo = typeof(T).GetProperty(groupColName);
        var _GroupByParameter = Expression.Parameter(typeof(T), "x");
        var _GroupByProperty = Expression
                .Property(_GroupByParameter, _GroupByPropInfo);
        var _GroupByLambda = Expression.Lambda<Func<T, TGroupCol>>
            (_GroupByProperty, new ParameterExpression[] { _GroupByParameter });
    
        // (x => x.valueColName)
        var _SelectParameter = Expression.Parameter(typeof(T), "x");
        var _SelectProperty = Expression
                .Property(_SelectParameter, valueColName);
        var _SelectLambda = Expression.Lambda<Func<T, TValueCol>>
            (_SelectProperty, new ParameterExpression[] { _SelectParameter });
    
        // return list.GroupBy(x => x.groupColName)
        //   .Select(g => g.OrderByDescending(x => x.valueColName).First());
        return list.GroupBy(_GroupByLambda)
            .Select(g => g.OrderByDescending(_SelectLambda.Compile()).First());
    }
    

    如您所见,我在扩展方法之前添加了一个下划线。当然,您不需要这样做。只需接受一般的想法并按照它运行。

    你可以这样称呼它:

    public class Item
    {
        public string Family { get; set; }
        public int Value { get; set; }
    }
    
    foreach (Item item in _List
            .AsQueryable()._GroupMaxs<Item, String, int>("Family", "Value"))
        Console.WriteLine("{0}:{1}", item.Family, item.Value);
    

    祝你好运!

    【讨论】:

    • 这看起来很聪明也很有帮助,但对于我的目的来说可能有点矫枉过正。我会为这项技术添加书签,以防我必须排除任何其他方法,但现在可能更容易直接编写代码。
    • @ctkrohn,welp,你在你的问题中要求这种方法感到羞耻。如果我选择的话,我也会内联编写它。但是对于您的原始问题,我没有看到更优雅的答案。我想,C'est la vie。
    • 感谢您花时间编写我懒得提供的代码。
    猜你喜欢
    • 2022-08-20
    • 1970-01-01
    • 1970-01-01
    • 2021-11-05
    • 2010-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多