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

509

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

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

Теги:
linq

11 ответов

724
Лучший ответ

Простейшим для понимания определением 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, которое было целью этого ответа. Однако, если использовать этот метод для фактического создания большого количества разделенных запятыми данных, было бы более целесообразно использовать 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

  • 6
    К сожалению, вы описываете только одну из перегрузок. Если бы вы добавили примеры таким же образом для других перегрузок (хотя бы один пример с начальным числом), ваш ответ был бы действительно великолепным.
  • 13
    @mbx - третий пример добавлен, чтобы продемонстрировать простой пример с seed. Спасибо за ответ.
Показать ещё 7 комментариев
106

Отчасти это зависит от того, о какой перегрузке вы говорите, но основная идея:

  • Начните с семени как "текущее значение"
  • Итерации по последовательности. Для каждого значения в последовательности:
    • Применить заданную пользователем функцию для преобразования (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, которые разделяют элементы на дерево, чтобы можно было распределить работу между ядрами? Кажется, что дизайн метода согласуется с понятиями «уменьшить» или «свернуть», но я не знаю, действительно ли он делает это под капотом или просто перебирает список элементов.
  • 0
    @Jon: вышеупомянутый edulink не работает, вы можете перенаправить меня на нужную ссылку. И не могли бы вы быть более конкретным в отношении термина «специализированные» функции агрегации, который вы использовали в своем ответе.
Показать ещё 1 комментарий
52

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

Немного длиннее .Max(),.Min(),.Sum(),.Average() выполняет итерацию по элементам в последовательности и агрегирует их с использованием соответствующей агрегатной функции..Aggregate() является обобщенным агрегатором, поскольку он позволяет разработчику указывать начальное состояние (aka seed) и агрегатную функцию.

Я знаю, что вы попросили краткое объяснение, но я подумал, что другие дали несколько коротких ответов, которые, по моему мнению, вам интереснее немного длиннее.

Длинная версия с кодом Один из способов проиллюстрировать, что это может показать, как вы реализуете Sample Standard Deviation один раз, используя 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;

Versus:

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

Итак, что. Агрегат - это то, что он инкапсулирует этот шаблон агрегатора, и я ожидаю, что реализация .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);

ИМХО

Так делает. Объясняет читаемость справки? В общем, я люблю LINQ, потому что думаю. Где бы то ни было .Select,.OrderBy и т.д. Значительно облегчает читаемость (если вы избегаете встроенных иерархий.Выбирает). Агрегат должен быть в Linq по причинам полноты, но лично я не настолько убежден, что. Агрегат добавляет читаемость по сравнению с хорошо написанным foreach.

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

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

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

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

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

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

описание переменных

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

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

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

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

аргументы:

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

второй аргумент является func, который является func, который принимает 2 int.

1.Это будет сохраняться так же, как и до значения суммирования (агрегированного значения), возвращаемого func после вычисления.

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

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

12

Изображение стоит тысячи слов

Напоминание: Func<A, B, C> - это функция с двумя входами типа A и B, которая возвращает C.

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


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

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

Изображение 1044

Пример:

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)

Изображение 1045

Пример:

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, за которой следует функция, которая преобразует ее результат.


Иллюстрации адаптированы из этого превосходного blogpost.

  • 0
    Это был бы отличный ответ .... на вопрос о Хаскеле. Но в .net нет перегрузки Aggegate которая принимает Func<T, T, T> .
  • 3
    Да, есть . Вы используете это в своем собственном ответе!
Показать ещё 1 комментарий
6

Выучил много из ответ Жамеца.

Если нужна только генерация CSV-строки, вы можете попробовать это.

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

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

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

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

1

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

Если преобразование реализовано как 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 в качестве примера;

0

Каждый дал свое объяснение. Мое объяснение таково.

Агрегатный метод применяет функцию к каждому элементу коллекции. Например, пусть набор {6, 2, 8, 3} и функция Add (оператор +) он делает (((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; }
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); здесь требуется семя, чтобы использовать вторую перегрузку.

        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
0

Это объяснение использования Aggregate в Fluent API, таком как сортировка Linq.

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню