Объясненный алгоритм агрегирования LINQ

723

Это может звучать глупо, но я не смог найти действительно хорошего объяснения Aggregate.

Хороший означает короткий, описательный, всеобъемлющий с небольшим и ясным примером.

Александр Белецкий
источник

Ответы:

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)

Это добавляет 1и 2сделать 3. Затем добавляет 3(результат предыдущего) и 3(следующий элемент в последовательности), чтобы сделать 6. Затем добавляет 6и 4сделать 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

Jamiec
источник
11
Другое объяснение первого описания состоит в том, что предоставляемая вами функция всегда объединяет первые два члена, пока массив не будет сокращен до одного элемента. Так [1,2,3,4]будет [3,3,4]потом [6,4]и наконец [10]. Но вместо того, чтобы возвращать массив одного значения, вы просто получаете само значение.
Дэвид Рааб
2
Могу ли я досрочно прервать / выйти из агрегатной функции? Например, chars.Aggregate ((a, b) => {if (a == 'a') разбивает весь агрегат, иначе возвращает a + ',' + b})
Джефф Тиан
13
@JeffTian - я бы предложил соединить последовательно TakeWhileтогда Aggregate- то будет beatuty расширений перечислимого - они легко цепные. Итак, вы в конечном итоге TakeWhile(a => a == 'a').Aggregate(....). Посмотрите этот пример: rextester.com/WPRA60543
Jamiec
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 не работает, вы можете перенаправить меня на нужную ссылку. И не могли бы вы быть более конкретным в отношении термина «специализированные» функции агрегации, который вы использовали в своем ответе.
Кушик,
1
@Koushik: я исправил ссылку в посте. Под «специализированными» функциями агрегации я подразумеваю такие вещи, как Макс / Мин / Счет / Сумма.
Джон Скит
62

Супер короткий Агрегат работает как фолд в Haskell / ML / F #.

Немного длиннее .Max (), .Min (), .Sum (), .Average () все перебирает элементы в последовательности и агрегирует их, используя соответствующую функцию агрегирования. .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). Агрегат должен быть в Linq по причинам полноты, но лично я не настолько убежден, что .Agregate добавляет удобочитаемость по сравнению с хорошо написанным foreach.

Просто еще один метапрограммист
источник
+1 Отлично! Но методов расширения SampleStandardDeviation_Aggregate()и SampleStandardDeviation_ForEach()не может быть private(по умолчанию при отсутствии квалификатора доступа), поэтому должен был быть накоплен либо, publicлибо internal, как мне кажется
Fulproof
К вашему сведению: если я правильно помню, методы расширения в моем примере были частью того же класса, который использовал их ==> частные работы в этом случае.
просто еще один метапрограммист
39

Одна картинка стоит тысячи слов

Напоминание:
Func<X, Y, R>это функция с двумя входами типа Xи Y, которая возвращает результат типа R.

Enumerable.Aggregate имеет три перегрузки:


Перегрузка 1:

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

Aggregate1

Пример:

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)

Aggregate2

Пример:

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)


Третья перегрузка не очень полезна ИМО.
То же самое можно записать более кратко, используя перегрузку 2, за которой следует функция, которая преобразует свой результат.


Иллюстрации взяты из этого отличного поста .

3dGrabber
источник
Это было бы отличным ответом .... на вопрос о Хаскеле. Но Aggegateв .net нет перегрузки, которая принимает Func<T, T, T>.
Jamiec
4
Да, есть . Вы используете это в своем собственном ответе!
3dGrabber
1
Upvoting, потому что вы тщательно описываете, что происходит, когда последовательность пуста. Пусть N будет количеством элементов в источнике. Заметим, что перегрузка, которая не принимает a seed, применяет функцию аккумулятора N -1 раз; в то время как другие перегрузки (что делать потратьте seed) применить агрегатную функцию N раз.
Джепп Стиг Нильсен
17

Агрегат в основном используется для группировки или суммирования данных.

Согласно MSDN «Агрегатная функция применяет функцию накопителя к последовательности».

Пример 1: Добавить все числа в массив.

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

* важно: начальное совокупное значение по умолчанию - 1 элемент в последовательности сбора. т.е.: общее начальное значение переменной будет 1 по умолчанию.

объяснение переменной

total: он будет содержать итоговое значение (агрегированное значение), возвращаемое функцией.

nextValue: это следующее значение в последовательности массива. Это значение затем добавляется к агрегированному значению, т.е.

Пример 2: Добавить все элементы в массив. Также установите начальное значение аккумулятора, чтобы начать добавлять с 10.

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

Аргументация объяснения:

первый аргумент - это начальный (начальное значение, т.е. начальное значение), который будет использоваться для начала сложения со следующим значением в массиве.

Второй аргумент - это func, который принимает 2 int.

1.total: он будет таким же, как и до суммирования (агрегированного значения), возвращаемого функцией после вычисления.

2.nextValue :: это следующее значение в последовательности массивов. Это значение затем добавляется к агрегированному значению, т.е.

Также отладка этого кода даст вам лучшее понимание того, как работает агрегат.

maxspan
источник
7

Много узнал из ответа Джеймица .

Если вам нужно только создать строку CSV, вы можете попробовать это.

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

Вот тест с 1 миллионом строк

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

Исходный код здесь

Rm558
источник
Когда я запустил в dotnetfiddle.net тот же код, что и в ссылке, я получил «Неустранимая ошибка: превышен лимит использования памяти» для «string.Join», но Aggregate всегда работал как положено. Поэтому я считаю, что не рекомендуется использовать String.Join
Manish Jain
Странный? Когда я прокомментировал Первый секундомер, который был для Агрегата; тогда я не получаю «Фатальная ошибка: превышен лимит использования памяти». Пожалуйста, объясни! Ссылка: dotnetfiddle.net/6YyumS
Маниш Джайн
dotnetfiddle.net имеет ограничение памяти при достижении остановки выполнения. если вы перемещаете агрегатный код перед кодом String.Join, вы можете получить ошибку для агрегатного кода.
Rm558
7

В дополнение ко всем отличным ответам здесь, я также использовал это, чтобы пройти элемент через ряд шагов преобразования.

Если преобразование реализовано как a Func<T,T>, вы можете добавить несколько преобразований в a 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

Определение

Агрегатный метод - это метод расширения для универсальных коллекций. Метод агрегирования применяет функцию к каждому элементу коллекции. Не только применяет функцию, но и принимает ее результат в качестве начального значения для следующей итерации. Таким образом, в результате мы получим вычисленное значение (min, max, avg или другое статистическое значение) из коллекции.

Следовательно, метод Aggregate является формой безопасной реализации рекурсивной функции.

Безопасно , потому что рекурсия будет повторяться для каждого элемента коллекции, и мы не можем получить приостановку бесконечного цикла из-за неправильного условия выхода. Рекурсивно , потому что результат текущей функции используется в качестве параметра для следующего вызова функции.

Синтаксис:

collection.Aggregate(seed, func, resultSelector);
  • seed - начальное значение по умолчанию;
  • func - наша рекурсивная функция Это может быть лямбда-выражение, делегат 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
YuriUn
источник
Довольно хороший первый ответ. Отлично сработано! Позор, это такой старый вопрос, иначе у тебя было бы много голосов
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);
Jaider
источник
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.

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
источник
0

Краткое и существенное определение может быть следующим: метод расширения Linq Aggregate позволяет объявить своего рода рекурсивную функцию, применяемую к элементам списка, операндов которых два: элементы в том порядке, в котором они присутствуют в списке, один элемент за раз, и результат предыдущей рекурсивной итерации или ничего, если еще не рекурсия.

Таким образом, вы можете вычислить факториал чисел или объединить строки.

Сиро Корвино
источник
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()
                );

Select with index используется в функции Aggregate для суммирования совпадающих столбцов и возврата нового массива; {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); здесь семя необходимо для того, чтобы использовать вторую перегрузку.

        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
Дэн М
источник