LINQ聚合算法介绍


721

这听起来有些la脚,但我一直无法找到关于的很好的解释Aggregate

好的意味着简短,描述性的,全面的,并带有一个小而清晰的例子。

Answers:


1015

最容易理解的定义Aggregate是,它考虑到之前进行的操作,对列表的每个元素执行操作。也就是说,它在第一个和第二个元素上执行操作并将结果转发。然后,它对先前的结果和第三个元素进行运算并继续进行。等等

例子1.求和

var nums = new[]{1,2,3,4};
var sum = nums.Aggregate( (a,b) => a + b);
Console.WriteLine(sum); // output: 10 (1+2+3+4)

这增加12使3。然后添加3(上一个的结果)和3(顺序的下一个元素)来制作6。然后添加64制作10

例子2.从一个字符串数组创建一个csv

var chars = new []{"a","b","c", "d"};
var csv = chars.Aggregate( (a,b) => a + ',' + b);
Console.WriteLine(csv); // Output a,b,c,d

运作方式大致相同。连接a一个逗号并b进行a,b。然后a,b 以逗号连接并c进行运算a,b,c。等等。

例子3.使用种子乘数字

为了完整起见Aggregate其中的重载带有种子值。

var multipliers = new []{10,20,30,40};
var multiplied = multipliers.Aggregate(5, (a,b) => a * b);
Console.WriteLine(multiplied); //Output 1200000 ((((5*10)*20)*30)*40)

与上述示例非常相似,此操作以值开始,5然后将其乘以序列的第一个元素,得出10结果50。将该结果结转并乘以序列中的下一个数字,得出20结果1000。这将继续执行序列的其余2个元素。

实时示例:http
: //rextester.com/ZXZ64749文档:http : //msdn.microsoft.com/en-us/library/bb548651.aspx


附录

上面的示例2使用字符串连接来创建用逗号分隔的值列表。这是一种简单的方式来说明Aggregate此答案的目的,以供使用。但是,如果使用此技术实际创建大量逗号分隔的数据,则使用a更合适StringBuilder,这与Aggregate使用种子重载来启动完全兼容StringBuilder

var chars = new []{"a","b","c", "d"};
var csv = chars.Aggregate(new StringBuilder(), (a,b) => {
    if(a.Length>0)
        a.Append(",");
    a.Append(b);
    return a;
});
Console.WriteLine(csv);

更新的示例:http : //rextester.com/YZCVXV6464


11
第一个描述的另一种解释是,您提供的函数将始终合并前两个成员,直到将数组缩小为一个元素为止。如此,最后[1,2,3,4]也将[3,3,4]如此。但是,您无需获取单个值的数组,而只需获取值本身即可。[6,4][10]
大卫·拉布

2
我可以提前中断/退出聚合功能吗?例如,chars.Aggregate((a,b)=> {if(a =='a')破坏整个聚合,否则返回a +','+ b})
Jeff Tian

13
@JeffTian-我建议链接一个TakeWhile然后Aggregate-就是Enumerable扩展的精髓-它们很容易链接。所以你最终得到了TakeWhile(a => a == 'a').Aggregate(....)。参见以下示例:rextester.com/WPRA60543
Jamiec 2014年

2
作为附录的注解,可以很容易地用var csv = string.Join(",", chars)(不需要聚集或字符串构建器)替换整个块-但是,是的,我知道答案的关键是给出聚集的示例用法,这样很酷。但我仍然想提一提,不建议仅将其用于连接字符串,已经有一种专用于此的方法...
T_D

2
另一个常见用法(到目前为止,我在生产代码中甚至没有看到的唯一用法)是获取最小或最大项,例如var biggestAccount = Accounts.Aggregate((a1, a2) => a1.Amount >= a2.Amount ? a1 : a2);
Franck

133

这部分取决于您要谈论的是哪种重载,但是基本思想是:

  • 从种子开始作为“当前值”
  • 遍历序列。对于序列中的每个值:
    • 应用用户指定的功能转换(currentValue, sequenceValue)(nextValue)
    • currentValue = nextValue
  • 返回决赛 currentValue

您可能会发现Aggregate我的Edulinq系列文章非常有用-它包含更详细的说明(包括各种重载)和实现。

一个简单的示例Aggregate用作替代Count

// 0 is the seed, and for each item, we effectively increment the current value.
// In this case we can ignore "item" itself.
int count = sequence.Aggregate(0, (current, item) => current + 1);

或者也许将一个字符串序列中的所有字符串长度求和:

int total = sequence.Aggregate(0, (current, item) => current + item.Length);

我个人很少发现Aggregate有用的-“量身定做”的汇总方法通常对我来说足够好。


6
@Jon是否有异步的Aggregate变体将项目拆分为树,以便可以在内核之间拆分工作?该方法的设计似乎与“减少”或“折叠”的概念一致,但是我不知道它是在幕后真正做到这一点,还是简单地遍历项目列表。
AaronLS

@Jon:上面提到的edulink无法正常工作,您可以将我重定向到正确的链接吗?您能否更具体地说明您在答案中使用的术语“量身定制的”聚合函数。
Koushik 2014年

1
@Koushik:我已经修复了帖子中的链接。所谓“量身定制”的聚合函数,我指的是最大/最小/计数/总和。
乔恩·斯基特

62

超短 聚合在Haskell / ML / F#中的作用类似于折叠。

稍长 。最大(),.Min(),.SUM(),。平均()并且使用相应的聚合函数在一个序列中的元素和它们的聚集体的所有迭代。.Aggregate()是广义的聚合器,因为它允许开发人员指定起始状态(即种子)和聚合函数。

我知道您要求简短的解释,但我认为其他人给出了一些简短的答案,我认为您可能会对更长的答案感兴趣

带代码的长版本 一种方法来说明它是什么,它可以说明您如何使用一次foreach和一次使用.Aggregate来实现示例标准偏差注意:我在这里没有优先考虑性能,因此我不必要地反复对大学进行了几次迭代

首先是用于创建二次距离之和的辅助函数:

static double SumOfQuadraticDistance (double average, int value, double state)
{
    var diff = (value - average);
    return state + diff * diff;
}

然后使用ForEach采样标准偏差:

static double SampleStandardDeviation_ForEach (
    this IEnumerable<int> ints)
{
    var length = ints.Count ();
    if (length < 2)
    {
        return 0.0;
    }

    const double seed = 0.0;
    var average = ints.Average ();

    var state = seed;
    foreach (var value in ints)
    {
        state = SumOfQuadraticDistance (average, value, state);
    }
    var sumOfQuadraticDistance = state;

    return Math.Sqrt (sumOfQuadraticDistance / (length - 1));
}

然后使用.Aggregate:

static double SampleStandardDeviation_Aggregate (
    this IEnumerable<int> ints)
{
    var length = ints.Count ();
    if (length < 2)
    {
        return 0.0;
    }

    const double seed = 0.0;
    var average = ints.Average ();

    var sumOfQuadraticDistance = ints
        .Aggregate (
            seed,
            (state, value) => SumOfQuadraticDistance (average, value, state)
            );

    return Math.Sqrt (sumOfQuadraticDistance / (length - 1));
}

请注意,除了如何计算sumOfQuadraticDistance之外,这些函数是相同的:

var state = seed;
foreach (var value in ints)
{
    state = SumOfQuadraticDistance (average, value, state);
}
var sumOfQuadraticDistance = state;

与:

var sumOfQuadraticDistance = ints
    .Aggregate (
        seed,
        (state, value) => SumOfQuadraticDistance (average, value, state)
        );

所以.Aggregate所做的是封装了该聚合器模式,我希望.Aggregate的实现看起来像这样:

public static TAggregate Aggregate<TAggregate, TValue> (
    this IEnumerable<TValue> values,
    TAggregate seed,
    Func<TAggregate, TValue, TAggregate> aggregator
    )
{
    var state = seed;

    foreach (var value in values)
    {
        state = aggregator (state, value);
    }

    return state;
}

使用标准偏差函数将如下所示:

var ints = new[] {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
var average = ints.Average ();
var sampleStandardDeviation = ints.SampleStandardDeviation_Aggregate ();
var sampleStandardDeviation2 = ints.SampleStandardDeviation_ForEach ();

Console.WriteLine (average);
Console.WriteLine (sampleStandardDeviation);
Console.WriteLine (sampleStandardDeviation2);

恕我直言

那么.Aggregate是否有助于提高可读性?总的来说,我喜欢LINQ,因为我认为.Where,.Select,.OrderBy等可以极大地提高可读性(如果避免使用内联的层次化.Selects)。出于完整性原因,Aggregate必须位于Linq中,但我个人并不相信.Aggregate与编写良好的foreach相比,增加了可读性。


+1好!但是扩展方法SampleStandardDeviation_Aggregate()SampleStandardDeviation_ForEach()不能private(默认情况下在没有访问限定符的情况下),所以应该由public或产生internal,在我看来
Fulproof 2013年

仅供参考:如果我没记错的话,示例中的扩展方法是使用它们的同一类的一部分==>在这种情况下为私有作品。
只是另一个元编程人员

39

一张图片胜过千言万语

提醒:
Func<X, Y, R>此函数具有类型为X和的两个输入Y,它返回类型为的结果R

Enumerable.Aggregate具有三个重载:


过载1:

A Aggregate<A>(IEnumerable<A> a, Func<A, A, A> f)

骨料1

例:

new[]{1,2,3,4}.Aggregate((x, y) => x + y);  // 10


此重载很简单,但是有以下限制:

  • 该序列必须至少包含一个元素,
    否则该函数将抛出InvalidOperationException
  • 元素和结果必须是同一类型。



过载2:

B Aggregate<A, B>(IEnumerable<A> a, B bIn, Func<B, A, B> f)

骨料2

例:

var hayStack = new[] {"straw", "needle", "straw", "straw", "needle"};
var nNeedles = hayStack.Aggregate(0, (n, e) => e == "needle" ? n+1 : n);  // 2


这种重载更为普遍:

  • 必须提供种子值(bIn)。
  • 集合可以为空,
    在这种情况下,该函数将产生种子值作为结果。
  • 元素和结果可以具有不同的类型。



过载3:

C Aggregate<A,B,C>(IEnumerable<A> a, B bIn, Func<B,A,B> f, Func<B,C> f2)


第三次过载不是非常有用的IMO。
通过使用重载2及其后的函数对其结果进行转换,可以更简洁地编写相同的内容。


插图取材自这篇出色的博客文章


对于有关Haskel的问题,这将是一个很好的答案。但是Aggegate.net中没有过载需要Func<T, T, T>
Jamiec '17

4
是的,有。您可以在自己的答案中使用它!
3dGrabber

1
赞成,因为您仔细描述了序列为空时会发生什么。令N为源中元素的数量。我们观察到不带a的过载seed应用N -1次累加器功能;而其他重载(确实需要seed)会应用N次累加器功能。
Jeppe Stig Nielsen,

17

聚合基本上用于对数据进行分组或汇总。

根据MSDN,“聚合函数在序列上应用累加器函数”。

示例1:将所有数字加到数组中。

int[] numbers = new int[] { 1,2,3,4,5 };
int aggregatedValue = numbers.Aggregate((total, nextValue) => total + nextValue);

*重要:默认情况下,初始合计值是收集顺序中的1个元素。即:默认情况下,总变量初始值将为1。

变量说明

total:它将保存func返回的总和(汇总值)。

nextValue:它是数组序列中的下一个值。然后将该值添加到汇总值(即总计)中。

示例2:将所有项目添加到数组中。还将初始累加器值设置为从10开始加法。

int[] numbers = new int[] { 1,2,3,4,5 };
int aggregatedValue = numbers.Aggregate(10, (total, nextValue) => total + nextValue);

参数说明:

第一个参数是初始值(起始值,即种子值),该值将用于开始与数组中的下一个值相加。

第二个参数是一个func,它是一个需要2 int的func。

1.total:与计算后函数返回的总和(汇总值)相同。

2.nextValue::它是数组序列中的下一个值。然后将该值添加到汇总值(即总计)中。

同时调试此代码将使您对聚合的工作方式有更好的了解。


7

Jamiec的答案中学到很多东西。

如果唯一需要生成CSV字符串,则可以尝试此操作。

var csv3 = string.Join(",",chars);

这是一百万个字符串的测试

0.28 seconds = Aggregate w/ String Builder 
0.30 seconds = String.Join 

源代码在这里


当我在dotnetfiddle.net中按链接中提供的方式运行相同的代码时,出现了“严重错误:超出了内存使用限制”的“ string.Join”,但Aggregate始终按预期工作。因此,我相信不建议您使用String.Join
Manish Jain

奇怪?当我评论“聚集”的“第一站”时;那么我没有得到任何“致命错误:超出了内存使用限制”。请解释!链接:dotnetfiddle.net/6YyumS
Manish Jain

当到达执行停止位置时,dotnetfiddle.net具有内存限制。如果将汇总代码移到String.Join代码之前,则汇总可能会出错。
RM558

7

除了这里已经给出的所有出色答案之外,我还使用它来引导项目完成一系列转换步骤。

如果将转换实现为Func<T,T>,则可以将多个转换添加到,List<Func<T,T>>并用于遍历每个步骤Aggregate的实例T

一个更具体的例子

您想要一个string值,并逐步进行一系列可以以编程方式构建的文本转换。

var transformationPipeLine = new List<Func<string, string>>();
transformationPipeLine.Add((input) => input.Trim());
transformationPipeLine.Add((input) => input.Substring(1));
transformationPipeLine.Add((input) => input.Substring(0, input.Length - 1));
transformationPipeLine.Add((input) => input.ToUpper());

var text = "    cat   ";
var output = transformationPipeLine.Aggregate(text, (input, transform)=> transform(input));
Console.WriteLine(output);

这将创建一系列转换:删除前导和尾随空格->删除第一个字符->删除最后一个字符->转换为大写字母。可以根据需要添加,删除或重新排序此链中的步骤,以创建所需的任何类型的转换管道。

这种特定管道的最终结果,就是" cat "变成"A"


一旦你意识到这可能会变得非常强大T可以任何东西。例如,它可以用于图像转换,例如滤镜BitMap


4

定义

聚合方法是通用集合的扩展方法。聚合方法将函数应用于集合的每个项目。不仅应用函数,而且将其结果用作下一次迭代的初始值。因此,结果是,我们将从集合中获得一个计算值(最小值,最大值,平均值或其他统计值)。

因此,聚合方法是一种安全实现递归函数的形式。

安全,因为递归将迭代集合的每个项目,并且由于错误的退出条件,我们无法获得任何无限循环暂停。递归的,因为当前函数的结果用作下一个函数调用的参数。

句法:

collection.Aggregate(seed, func, resultSelector);
  • 种子 -默认情况下的初始值;
  • func-我们的递归函数。它可以是lambda表达式,Func委托或函数类型TF(T result,T nextValue);
  • resultSelector-它可以是诸如func之类的函数,也可以是用于计算,转换,更改,转换最终结果的表达式。

怎么运行的:

var nums = new[]{1, 2};
var result = nums.Aggregate(1, (result, n) => result + n); //result = (1 + 1) + 2 = 4
var result2 = nums.Aggregate(0, (result, n) => result + n, response => (decimal)response/2.0); //result2 = ((0 + 1) + 2)*1.0/2.0 = 3*1.0/2.0 = 3.0/2.0 = 1.5

实际用法:

  1. 从数字n中查找阶乘:

int n = 7;
var numbers = Enumerable.Range(1, n);
var factorial = numbers.Aggregate((result, x) => result * x);

该功能与此功能相同:

public static int Factorial(int n)
{
   if (n < 1) return 1;

   return n * Factorial(n - 1);
}
  1. Aggregate()是最强大的LINQ扩展方法之一,例如Select()和Where()。我们可以用它来代替Sum(),Min()。Max(),Avg()功能,或通过实现附加上下文对其进行更改:
    var numbers = new[]{3, 2, 6, 4, 9, 5, 7};
    var avg = numbers.Aggregate(0.0, (result, x) => result + x, response => (double)response/(double)numbers.Count());
    var min = numbers.Aggregate((result, x) => (result < x)? result: x);
  1. 扩展方法的更复杂用法:
    var path = @“c:\path-to-folder”;

    string[] txtFiles = Directory.GetFiles(path).Where(f => f.EndsWith(“.txt”)).ToArray<string>();
    var output = txtFiles.Select(f => File.ReadAllText(f, Encoding.Default)).Aggregate<string>((result, content) => result + content);

    File.WriteAllText(path + summary.txt”, output, Encoding.Default);

    Console.WriteLine(“Text files merged into: {0}”, output); //or other log info

很好的第一个答案。做得好!真可惜,这是一个古老的问题,否则您会受到很多反对
Jamiec

1

这是有关Aggregate在Fluent API(例如Linq Sorting)上使用的说明。

var list = new List<Student>();
var sorted = list
    .OrderBy(s => s.LastName)
    .ThenBy(s => s.FirstName)
    .ThenBy(s => s.Age)
    .ThenBy(s => s.Grading)
    .ThenBy(s => s.TotalCourses);

并让我们看到我们想要实现一个接受一组字段的排序函数,这很容易使用,Aggregate而不是像for循环这样的:

public static IOrderedEnumerable<Student> MySort(
    this List<Student> list,
    params Func<Student, object>[] fields)
{
    var firstField = fields.First();
    var otherFields = fields.Skip(1);

    var init = list.OrderBy(firstField);
    return otherFields.Skip(1).Aggregate(init, (resultList, current) => resultList.ThenBy(current));
}

我们可以这样使用它:

var sorted = list.MySort(
    s => s.LastName,
    s => s.FirstName,
    s => s.Age,
    s => s.Grading,
    s => s.TotalCourses);

1

大家都给了他解释。我的解释就是这样。

聚合方法将函数应用于集合的每个项目。例如,让我们拥有集合{6,2,8,3}和它执行的函数Add(operator +)((((6 + 2)+8)+3)并返回19

var numbers = new List<int> { 6, 2, 8, 3 };
int sum = numbers.Aggregate(func: (result, item) => result + item);
// sum: (((6+2)+8)+3) = 19

在此示例中,传递了名为Add的方法,而不是lambda表达式。

var numbers = new List<int> { 6, 2, 8, 3 };
int sum = numbers.Aggregate(func: Add);
// sum: (((6+2)+8)+3) = 19

private static int Add(int x, int y) { return x + y; }

0

可能有一个简短而基本的定义:Linq聚合扩展方法允许声明一种应用于列表元素的递归函数,列表的元素的操作数为两个:元素在列表中的出现顺序为:一次只包含一个元素,则返回上一个递归迭代的结果;如果尚未递归,则为空。

这样,您可以计算数字的阶乘或连接字符串。


0

用于对多维整数数组中的列求和的聚合

        int[][] nonMagicSquare =
        {
            new int[] {  3,  1,  7,  8 },
            new int[] {  2,  4, 16,  5 },
            new int[] { 11,  6, 12, 15 },
            new int[] {  9, 13, 10, 14 }
        };

        IEnumerable<int> rowSums = nonMagicSquare
            .Select(row => row.Sum());
        IEnumerable<int> colSums = nonMagicSquare
            .Aggregate(
                (priorSums, currentRow) =>
                    priorSums.Select((priorSum, index) => priorSum + currentRow[index]).ToArray()
                );

在聚合函数中使用带有索引的选择来对匹配的列求和并返回一个新的数组;{3 + 2 = 5,1 + 4 = 5,7 + 16 = 23,8 + 5 = 13}。

        Console.WriteLine("rowSums: " + string.Join(", ", rowSums)); // rowSums: 19, 27, 44, 46
        Console.WriteLine("colSums: " + string.Join(", ", colSums)); // colSums: 25, 24, 45, 42

但是,由于累积类型(int)与源类型(bool)不同,因此在布尔数组中计算true的数量更加困难。为了使用第二个重载,这里需要种子。

        bool[][] booleanTable =
        {
            new bool[] { true, true, true, false },
            new bool[] { false, false, false, true },
            new bool[] { true, false, false, true },
            new bool[] { true, true, false, false }
        };

        IEnumerable<int> rowCounts = booleanTable
            .Select(row => row.Select(value => value ? 1 : 0).Sum());
        IEnumerable<int> seed = new int[booleanTable.First().Length];
        IEnumerable<int> colCounts = booleanTable
            .Aggregate(seed,
                (priorSums, currentRow) =>
                    priorSums.Select((priorSum, index) => priorSum + (currentRow[index] ? 1 : 0)).ToArray()
                );

        Console.WriteLine("rowCounts: " + string.Join(", ", rowCounts)); // rowCounts: 3, 1, 2, 2
        Console.WriteLine("colCounts: " + string.Join(", ", colCounts)); // colCounts: 3, 2, 1, 2
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.