Algoritmo agregado de LINQ explicado

722

Esto puede sonar cojo, pero no he podido encontrar una explicación realmente buena Aggregate.

Bueno significa breve, descriptivo, integral con un ejemplo pequeño y claro.

Alexander Beletsky
fuente

Respuestas:

1015

La definición más fácil de entender Aggregatees que realiza una operación en cada elemento de la lista teniendo en cuenta las operaciones anteriores. Es decir, realiza la acción en el primer y segundo elemento y lleva el resultado hacia adelante. Luego opera sobre el resultado anterior y el tercer elemento y continúa. etc.

Ejemplo 1. Sumar números

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

Esto agrega 1y 2hacer 3. Luego agrega 3(resultado del anterior) y 3(siguiente elemento en secuencia) para hacer 6. Luego agrega 6y 4para hacer 10.

Ejemplo 2. crear un csv a partir de una matriz de cadenas

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

Esto funciona de la misma manera. Concatenar auna coma y bhacer a,b. Luego concatena a,b con una coma y chacer a,b,c. y así.

Ejemplo 3. Multiplicar números usando una semilla

Para completar, hay una sobrecarga de la Aggregatecual toma un valor semilla.

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)

Al igual que los ejemplos anteriores, esto comienza con un valor de 5y lo multiplica por el primer elemento de la secuencia 10dando un resultado de 50. Este resultado se lleva adelante y se multiplica por el siguiente número en la secuencia 20para dar un resultado 1000. Esto continúa a través de los 2 elementos restantes de la secuencia.

Ejemplos en vivo: http://rextester.com/ZXZ64749
Documentos: http://msdn.microsoft.com/en-us/library/bb548651.aspx


Apéndice

El ejemplo 2 anterior usa la concatenación de cadenas para crear una lista de valores separados por una coma. Esta es una forma simplista de explicar el uso de Aggregatecuál fue la intención de esta respuesta. Sin embargo, si usa esta técnica para crear una gran cantidad de datos separados por comas, sería más apropiado usar a StringBuilder, y esto es totalmente compatible con el Aggregateuso de la sobrecarga sembrada para iniciar el 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);

Ejemplo actualizado: http://rextester.com/YZCVXV6464

Jamiec
fuente
11
Otra explicación para la primera descripción es que la función que proporciona siempre combina los dos primeros miembros hasta que la matriz se reduce a un elemento. Así [1,2,3,4]será [3,3,4]entonces [6,4]y por fin [10]. Pero en lugar de devolver una matriz de un solo valor, solo obtiene el valor en sí.
David Raab
2
¿Puedo romper / salir antes de tiempo de una función Agregado? Por ejemplo, chars.Aggregate ((a, b) => {if (a == 'a') rompe todo el agregado de lo contrario devuelve a + ',' + b})
Jeff Tian
13
@JeffTian - Sugeriría encadenar un TakeWhileentonces un Aggregate- eso es lo mejor de Enumerable extensiones - son fácilmente encadenables. Entonces terminas con TakeWhile(a => a == 'a').Aggregate(....). Vea este ejemplo: rextester.com/WPRA60543
Jamiec
2
Como nota al margen del apéndice, todo el bloque podría reemplazarse fácilmente por var csv = string.Join(",", chars)(sin necesidad de agregados o constructores de cadenas), pero sí, sé que el punto de la respuesta era dar un ejemplo de uso de agregado, por lo que es genial. Pero aún quería mencionar que no se recomienda solo para unir cadenas, ya hay un método dedicado para eso ...
T_D
2
Otro uso común (hasta ahora lo único que he visto en el código de producción) es obtener artículos mínimos o máximos comovar biggestAccount = Accounts.Aggregate((a1, a2) => a1.Amount >= a2.Amount ? a1 : a2);
Franck
133

Depende en parte de la sobrecarga de la que esté hablando, pero la idea básica es:

  • Comience con una semilla como el "valor actual"
  • Iterar sobre la secuencia. Para cada valor en la secuencia:
    • Aplicar una función especificada por el usuario para transformar (currentValue, sequenceValue)en(nextValue)
    • Conjunto currentValue = nextValue
  • Devuelve la final currentValue

Puede encontrar útil la Aggregatepublicación en mi serie Edulinq : incluye una descripción más detallada (incluidas las diversas sobrecargas) y las implementaciones.

Un ejemplo simple es usar Aggregatecomo alternativa a 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);

O quizás sumando todas las longitudes de cadenas en una secuencia de cadenas:

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

Personalmente, rara vez me parece Aggregateútil: los métodos de agregación "a medida" suelen ser lo suficientemente buenos para mí.

Jon Skeet
fuente
66
@ Jon ¿Hay variaciones asíncronas de Agregado que dividen los elementos en un árbol para que el trabajo se pueda dividir entre núcleos? Parece que el diseño del método es consistente con los conceptos de "reducir" o "doblar", pero no sé si realmente lo está haciendo bajo el capó, o simplemente iterando a través de la lista de elementos.
AaronLS
@ Jon: el edulink mencionado anteriormente no funciona, ¿puede redirigirme al enlace correcto? ¿Y puede ser más específico sobre el término funciones de agregación "a medida" que utilizó en su respuesta.
Koushik
1
@Koushik: He arreglado el enlace en la publicación. Por funciones de agregación "a medida" me refiero a cosas como Max / Min / Count / Sum.
Jon Skeet
62

El agregado súper corto funciona como un pliegue en Haskell / ML / F #.

Un poco más largos .Max (), .Min (), .Sum (), .Average () todos iteran sobre los elementos en una secuencia y los agregan usando la función de agregado respectiva. .Aggregate () es un agregador generalizado en el sentido de que permite al desarrollador especificar el estado de inicio (también conocido como semilla) y la función de agregado.

Sé que pediste una breve explicación, pero pensé que mientras otros daban un par de respuestas cortas, pensé que tal vez te interesaría una más larga.

Versión larga con código Una forma de ilustrar qué puede hacer es mostrar cómo implementa la Desviación estándar de muestra una vez que usa foreach y una vez que usa .Aggregate. Nota: No he priorizado el rendimiento aquí, así que repito varias veces innecesariamente la recopilación.

Primero, una función auxiliar utilizada para crear una suma de distancias cuadráticas:

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

Luego, muestra la desviación estándar usando 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));
}

Luego, una vez que use .Agregar:

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

Tenga en cuenta que estas funciones son idénticas, excepto cómo se calcula sumOfQuadraticDistance:

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

Versus:

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

Entonces, lo que hace .Aggregate es que encapsula este patrón de agregación y espero que la implementación de .Aggregate se vea más o menos así:

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

El uso de las funciones de desviación estándar se vería así:

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

En mi humilde opinión

Entonces, ¿Agregado ayuda a la legibilidad? En general, me encanta LINQ porque creo que .Where, .Select, .OrderBy, etc., ayuda enormemente a la legibilidad (si evita las .selects jerárquicas en línea). Agregado tiene que estar en Linq por razones de integridad, pero personalmente no estoy tan convencido de que. Agregado agrega legibilidad en comparación con un foreach bien escrito.

Solo otro metaprogramador
fuente
+1 Excelente! Sin embargo, los métodos de extensión SampleStandardDeviation_Aggregate()y SampleStandardDeviation_ForEach()no pueden ser private(por defecto en ausencia de un calificador de acceso), por lo que debería haber sido acumulados por cualquiera publico internal, me parece
Fulproof
FYI: Si recuerdo correctamente, los métodos de extensión en mi muestra eran parte de la misma clase que los usó ==> trabajos privados en este caso.
Solo otro metaprogramador
39

Una imagen vale mas que mil palabras

Recordatorio:
Func<X, Y, R>es una función con dos entradas de tipo Xy Y, que devuelve un resultado de tipo R.

Enumerable.Aggregate tiene tres sobrecargas:


Sobrecarga 1:

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

Agregado1

Ejemplo:

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


Esta sobrecarga es simple, pero tiene las siguientes limitaciones:

  • la secuencia debe contener al menos un elemento, de lo
    contrario la función arrojará un InvalidOperationException.
  • Los elementos y el resultado deben ser del mismo tipo.



Sobrecarga 2:

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

Agregado2

Ejemplo:

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


Esta sobrecarga es más general:

  • se debe proporcionar un valor semilla ( bIn).
  • la colección puede estar vacía,
    en este caso, la función generará el valor de semilla como resultado.
  • Los elementos y el resultado pueden tener diferentes tipos.



Sobrecarga 3:

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


La tercera sobrecarga no es muy útil IMO.
Lo mismo se puede escribir de manera más sucinta mediante el uso de sobrecarga 2 seguido de una función que transforma su resultado.


Las ilustraciones están adaptadas de este excelente blog .

3dGrabber
fuente
Esta sería una gran respuesta ... sobre una pregunta sobre Haskel. Pero no hay sobrecarga de Aggegateen .net que toma un Func<T, T, T>.
Jamiec
44
Sí hay . ¡Lo usas en tu propia respuesta!
3dGrabber
1
Votación positiva porque describe cuidadosamente lo que sucede cuando la secuencia está vacía. Sea N el número de elementos en la fuente. Observamos que la sobrecarga que no toma a seed, aplica la función de acumulador N -1 veces; mientras que las otras sobrecargas (que no toman una seed) aplicar la función de acumulador de N veces.
Jeppe Stig Nielsen
17

El agregado se usa básicamente para agrupar o resumir datos.

De acuerdo con MSDN "La función de agregado aplica una función de acumulador sobre una secuencia".

Ejemplo 1: Agregar todos los números en una matriz.

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

* importante: el valor agregado inicial por defecto es el elemento 1 en la secuencia de la colección. es decir: el valor inicial de la variable total será 1 por defecto.

explicación variable

total: mantendrá el valor de suma (valor agregado) devuelto por la función.

nextValue: es el siguiente valor en la secuencia de la matriz. Este valor se agrega al valor agregado, es decir, total.

Ejemplo 2: Agregar todos los elementos en una matriz. Establezca también el valor inicial del acumulador para comenzar a agregar desde 10.

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

explicación de argumentos:

El primer argumento es el inicial (valor inicial, es decir, valor inicial) que se utilizará para iniciar la adición con el siguiente valor en la matriz.

El segundo argumento es una función que es una función que requiere 2 int.

1.total: esto se mantendrá igual que antes del valor de suma (valor agregado) devuelto por el func después del cálculo.

2.nextValue:: es el siguiente valor en la secuencia de la matriz. Este valor se agrega al valor agregado, es decir, total.

Además, la depuración de este código le dará una mejor comprensión de cómo funciona el agregado.

maxspan
fuente
7

Aprendí mucho de la respuesta de Jamiec .

Si la única necesidad es generar una cadena CSV, puede intentarlo.

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

Aquí hay una prueba con 1 millón de cadenas

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

El código fuente está aquí.

Rm558
fuente
Cuando ejecuté el mismo código en dotnetfiddle.net que se proporciona en el enlace, recibí "Error grave: se excedió el límite de uso de memoria" para "string.Join" pero Aggregate siempre funcionó como se esperaba. Así que creo que no se recomienda usar String.Join
Manish Jain el
¿Extraño? Cuando comenté el primer cronómetro que era para Aggregate; entonces no obtengo ningún "Error grave: se excedió el límite de uso de memoria". ¡Por favor explique! Enlace: dotnetfiddle.net/6YyumS
Manish Jain
dotnetfiddle.net tiene un límite de memoria, cuando se detiene la ejecución. si mueve el código agregado antes del código String.Join, puede obtener un error para el agregado.
Rm558
7

Además de todas las excelentes respuestas aquí, también lo he usado para guiar un artículo a través de una serie de pasos de transformación.

Si una transformación se implementa como a Func<T,T>, puede agregar varias transformaciones a ay List<Func<T,T>>usar Aggregatepara recorrer una instancia de Tcada paso.

Un ejemplo mas concreto

Desea tomar un stringvalor y recorrerlo a través de una serie de transformaciones de texto que podrían construirse mediante programación.

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

Esto creará una cadena de transformaciones: eliminar espacios iniciales y finales -> eliminar el primer carácter -> eliminar el último carácter -> convertir a mayúsculas. Los pasos en esta cadena se pueden agregar, eliminar o reordenar según sea necesario, para crear cualquier tipo de tubería de transformación que se requiera.

El resultado final de esta tubería específica, es que se " cat "convierte "A".


Esto puede volverse muy poderoso una vez que te das cuenta de que Tpuede ser cualquier cosa . Esto podría usarse para transformaciones de imágenes, como filtros, usando BitMapcomo ejemplo;

Bradley Uffner
fuente
4

Definición

El método agregado es un método de extensión para colecciones genéricas. El método agregado aplica una función a cada elemento de una colección. No solo aplica una función, sino que toma su resultado como valor inicial para la próxima iteración. Por lo tanto, como resultado, obtendremos un valor calculado (min, max, avg u otro valor estadístico) de una colección.

Por lo tanto, el método Agregado es una forma de implementación segura de una función recursiva.

Seguro , porque la recursión iterará sobre cada elemento de una colección y no podemos obtener ninguna suspensión de bucle infinito por una condición de salida incorrecta. Recursivo , porque el resultado de la función actual se usa como parámetro para la próxima llamada a la función.

Sintaxis:

collection.Aggregate(seed, func, resultSelector);
  • semilla - valor inicial por defecto;
  • func - nuestra función recursiva. Puede ser una expresión lambda, un delegado Func o una función tipo TF (resultado T, T nextValue);
  • resultSelector : puede ser una función como func o una expresión para calcular, transformar, cambiar y convertir el resultado final.

Cómo funciona:

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

Uso práctico:

  1. Encuentra Factorial de un número n:

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

que está haciendo lo mismo que esta función:

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

   return n * Factorial(n - 1);
}
  1. Aggregate () es uno de los métodos de extensión LINQ más potentes, como Select () y Where (). Podemos usarlo para reemplazar Sum (), Min (). Max (), Avg () funcionalidad, o para cambiarlo mediante la implementación de contexto de adición:
    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. Uso más complejo de los métodos de extensión:
    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
YuriUn
fuente
Bastante buena primera respuesta. ¡Bien hecho! Lástima que sea una pregunta tan antigua o habría recibido muchos votos a favor
Jamiec
1

Esta es una explicación sobre el uso Aggregateen una API Fluent como 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);

y veamos que queremos implementar una función de clasificación que tome un conjunto de campos, esto es muy fácil de usar en Aggregatelugar de un ciclo for, como este:

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

Y podemos usarlo así:

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

Todos han dado su explicación. Mi explicación es así.

El método agregado aplica una función a cada elemento de una colección. Por ejemplo, tengamos la colección {6, 2, 8, 3} y la función Agregar (operador +) que hace (((6 + 2) +8) +3) y devuelve 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

En este ejemplo, se pasa el método con nombre Add en lugar de la expresión 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; }
bizimunda
fuente
0

Una definición breve y esencial podría ser esta: el método de extensión Agregado de Linq permite declarar una especie de función recursiva aplicada a los elementos de una lista, cuyos operandos son dos: los elementos en el orden en que están presentes en la lista, un elemento a la vez, y el resultado de la iteración recursiva anterior o nada, si no es que aún, recursividad.

De esta forma, puede calcular el factorial de los números o concatenar cadenas.

Ciro Corvino
fuente
0

Agregado utilizado para sumar columnas en una matriz entera multidimensional

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

Seleccionar con índice se utiliza dentro de la función Agregado para sumar las columnas coincidentes y devolver una nueva matriz; {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

Pero contar el número de verdades en una matriz booleana es más difícil ya que el tipo acumulado (int) difiere del tipo fuente (bool); aquí es necesaria una semilla para usar la segunda sobrecarga.

        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
Dan M
fuente