关于c#:解释LINQ Aggregate算法

LINQ Aggregate algorithm explained

这听起来可能有点蹩脚,但我还没能找到一个很好的解释来解释Aggregate

好的意思是简短、描述性、综合性,有一个小而清晰的例子。


最容易理解的Aggregate的定义是,它对列表中的每个元素执行一个操作,同时考虑到以前的操作。也就是说,它对第一个元素和第二个元素执行操作,并将结果向前推进。然后,它对前面的结果和第三个元素进行操作并继续进行。等。

例1。求和数

1
2
3
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

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

这种方法的工作原理大致相同。将ab连接起来,生成a,b。然后用逗号将a,bc连接起来,生成a,b,c。等等。

例3。用种子乘以数字

为了完整性,需要种子值的Aggregate有一个过载。

1
2
3
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)

与上面的例子很相似,它以EDOCX1的值(25)开始,并乘以序列EDOCX1的第一个元素(17),得到EDOCX1的结果(27)。将该结果结转,并乘以序列20中的下一个数字,得出1000的结果。这将贯穿序列的其余2个元素。

现场示例:http://rexteter.com/zxz64749文档:http://msdn.microsoft.com/en-us/library/bb548651.aspx

补遗

上面的示例2使用字符串串联创建由逗号分隔的值列表。这是一个简单的方法来解释使用Aggregate这是这个答案的意图。但是,如果使用此技术实际创建大量逗号分隔的数据,则更适合使用StringBuilder,这与使用种子重载启动StringBuilderAggregate完全兼容。

1
2
3
4
5
6
7
8
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://rexteter.com/yzcvxv6464


这在一定程度上取决于你所说的超载情况,但基本的想法是:

  • 以种子作为"当前值"
  • 迭代序列。对于序列中的每个值:
    • 应用用户指定的函数将(currentValue, sequenceValue)转换为(nextValue)
    • 设置currentValue = nextValue
  • 返回最终的currentValue

您可能会发现我的edulinq系列中的Aggregate文章很有用——它包括更详细的描述(包括各种重载)和实现。

一个简单的例子是使用Aggregate作为Count的替代:

1
2
3
// 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);

或是将一系列字符串中的所有字符串长度相加:

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

就我个人而言,我很少发现Aggregate有用,"定制的"聚合方法对我来说通常足够好。


超短在haskell/ml/f中,集料的作用类似于折叠。

稍长.max()、.min()、.sum()、.average()都按顺序迭代元素,并使用各自的聚合函数聚合它们。.aggregate()是一个通用的聚合器,它允许开发人员指定启动状态(aka seed)和聚合函数。

我知道你要求一个简短的解释,但我想当其他人给出了几个简短的答案时,我想你可能会对一个稍微长一点的答案感兴趣。

带代码的长版本一种方法来说明它可以显示如何在使用foreach和使用.aggregate时实现样本标准差。注意:我在这里没有优先考虑性能,因此我不必要地在集合上重复多次

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

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

然后使用foreach对标准偏差进行采样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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外,这些函数是相同的:

1
2
3
4
5
6
var state = seed;
foreach (var value in ints)
{
    state = SumOfQuadraticDistance (average, value, state);
}
var sumOfQuadraticDistance = state;

对抗:

1
2
3
4
5
var sumOfQuadraticDistance = ints
    .Aggregate (
        seed,
        (state, value) => SumOfQuadraticDistance (average, value, state)
        );

那么.aggregate所做的是封装这个聚合器模式,我希望.aggregate的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

使用标准偏差函数的结果如下:

1
2
3
4
5
6
7
8
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);

IMHO

聚合有助于提高可读性吗?一般来说,我喜欢linq,因为我认为.where,.select,.orderby等对可读性有很大帮助(如果您避免使用内联的hierarchical.selects)。出于完整性的考虑,Aggregate必须在Linq中,但就我个人而言,我并不这么确信。


一幅画胜过千言万语。

Reminder: Func is a function with two inputs of type A and B, that returns a C.

Enumerable.Aggregate有三个重载:

< BR>过载1:

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

Aggregate1

例子:

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

< BR>

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

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

< BR>

过载2:

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

Aggregate2

例子:

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

< BR>

这种过载更为普遍:

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

< BR>

过载3:

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

< BR>

在我看来,第三次过载不是很有用。通过使用重载2和转换其结果的函数,可以更简洁地编写相同的内容。

< BR>

The illustrations are adapted from this excellent blogpost.


聚合主要用于对数据进行分组或汇总。

根据msdn"聚合函数对序列应用聚合函数。"

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

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

*重要提示:初始聚合值默认为集合序列中的1元素。即:总变量初始值默认为1。

变量解释

总计:它将保存func返回的合计值(聚合值)。

NextValue:它是数组序列中的下一个值。该值不等于合计值的总和。

示例2:添加数组中的所有项。同时将初始累加器值设置为从10开始相加。

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

参数说明:

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

第二个参数是func,它是一个func,取2 int。

1.合计:与计算后func返回的合计值(聚合值)之前相同。

2.NextValue::是数组序列中的下一个值。该值不等于合计值的总和。

此外,调试此代码将使您更好地了解聚合是如何工作的。


从杰米克的回答中学到了很多。

如果只需要生成csv字符串,可以尝试此操作。

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

这是一个有一百万根弦的测试

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

这里是源代码


除了这里所有的好答案之外,我还使用它来遍历一个项目,通过一系列转换步骤。

如果一个转换被实现为一个Func,您可以向一个List>添加几个转换,并使用Aggregate在每个步骤中遍历T的一个实例。

更具体的例子

您希望获得一个string值,并通过一系列可以通过编程构建的文本转换来完成它。

1
2
3
4
5
6
7
8
9
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作为示例的过滤器;


每个人都给出了他的解释。我的解释是这样的。

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

1
2
3
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表达式。

1
2
3
4
5
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; }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        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()
                );

在聚合函数中使用select with index对匹配列求和并返回一个新数组;3+2=5,1+4=5,7+16=23,8+5=13。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        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

这是关于在流畅的API(如LINQ排序)上使用Aggregate的解释。

1
2
3
4
5
6
7
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循环,如下所示:

1
2
3
4
5
6
7
8
9
10
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));
}

我们可以这样使用它:

1
2
3
4
5
6
var sorted = list.MySort(
    s => s.LastName,
    s => s.FirstName,
    s => s.Age,
    s => s.Grading,
    s => s.TotalCourses);

一个简短而基本的定义可能是:linq聚合扩展方法允许声明一种应用于列表元素的递归函数,其中操作数是两个:元素按它们在列表中的出现顺序排列,一次一个元素,以及上一次递归迭代的结果,如果没有,则不声明还没有递归。

通过这种方式,您可以计算数字的阶乘,或者连接字符串。