Пояснено алгоритм сукупності LINQ


721

Це може звучати кульгаво, але мені не вдалося знайти справді хорошого пояснення 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


11
Ще одне пояснення першого опису полягає в тому, що надана вами функція завжди поєднує перші два члени, поки масив не скорочується до одного елемента. Так [1,2,3,4]буде і [3,3,4]тоді, [6,4]і нарешті [10]. Але замість повернення масиву одного значення ви просто отримаєте саме значення.
Девід Рааб

2
Чи можу я рано перерватися / вийти з функції сукупності? Наприклад, chars.Aggregate ((a, b) => {if (a == 'a') розбиває весь сукупний інший return a + ',' + b})
Jeff Tian

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);
Франк

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 Чи існують асинхронні варіації агрегату, які розділяють елементи на дерево, щоб робота могла бути розділена між ядрами? Здається, дизайн методу відповідає поняттям "зменшити" або "скласти", але я не знаю, чи це насправді робиться під кришкою, або просто перебирається через список елементів.
AaronLS

@Jon: згаданий вище едулінк не працює, чи можете ви перенаправити мене на правильне посилання. І чи можете ви бути більш детальними щодо терміна "з урахуванням" функцій агрегації, який ви використовували у своїй відповіді.
Кушик

1
@Koushik: Я поправив посилання в публікації. Під "адаптованими" функціями агрегації я маю на увазі такі речі, як Max / Min / Count / Sum.
Джон Скіт

62

Супер короткий агрегат працює як складений в Haskell / ML / F #.

Трохи довше .Max (), .Min (), .Sum (), .Average () всі повторює елементи в послідовності та агрегує їх за допомогою відповідної функції сукупності. .Aggregate () - це узагальнений агрегатор тим, що дозволяє розробнику вказати стартовий стан (він же насіння) та функцію агрегату.

Я знаю, що ви попросили короткого пояснення, але я зрозумів, як інші дали пару коротких відповідей, я вважав, що вас, можливо, зацікавить трохи довший

Довга версія з кодом Один із способів проілюструвати, що це може показати, як ви реалізуєте 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;

Проти:

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, тому що я думаю. Агрегат повинен бути в Linq з міркувань повноти, але особисто я не настільки впевнений, що. Агрегат додає читабельності порівняно з добре написаним передбаченням.


+1 Відмінно! Але методів розширення SampleStandardDeviation_Aggregate()і SampleStandardDeviation_ForEach()не може бути private(за замовчуванням за відсутності кваліфікуючого доступу), тому повинні були бути нараховані або одним, publicабо internal, мені здається
Повноцінним

FYI: Якщо я добре пам'ятаю, методи розширення в моєму зразку були частиною того ж класу, який використовував їх ==> приватні роботи в цьому випадку.
Ще один метапрограмер

39

Малюнок вартує тисячі слів

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

Численні. Агрегат має три перевантаження:


Перевантаження 1:

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

Сукупність1

Приклад:

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


Ця перевантаження проста, але вона має такі обмеження:

  • послідовність повинна містити принаймні один елемент,
    інакше функція кине an 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)


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


Ілюстрації адаптовані з цього чудового блогу .


Це була б чудова відповідь .... на питання про Хаскель. Але немає перевантаження Aggegateв .net, яка займає a Func<T, T, T>.
Jamiec

4
Так, є . Ви використовуєте це у власній відповіді!
3dGrabber

1
Оновлення, оскільки ви ретельно описуєте, що відбувається, коли послідовність порожня. Нехай N - кількість елементів у джерелі. Ми спостерігаємо, що перевантаження, яка не приймає а 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.

змінне пояснення

Всього: воно буде містити суму підсумків (агреговану величину), повернуту функцією.

nextValue: це наступне значення в послідовності масиву. Ця величина перевищує сукупну величину, тобто загальну.

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

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

пояснення аргументів:

перший аргумент - це початкове (початкове значення, тобто початкове значення), яке буде використано для початку додавання із наступним значенням у масиві.

другий аргумент - функція, яка є функцією, яка займає 2 int.

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

2.nextValue:: це наступне значення в послідовності масиву. Ця величина перевищує сукупну величину, тобто загальну.

Також налагодження цього коду дасть вам краще розуміння того, як працює сукупність.


7

Багато чого дізнався з відповіді Джейміка .

Якщо єдиною необхідністю є створення рядка CSV, ви можете спробувати це.

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

Ось тест з 1 мільйон рядків

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

Вихідний код тут


Коли я запустив той самий код на dotnetfiddle.net , що і у посиланні, я отримав "Fatal Error: Limits use memory was перевищено" для "string.Join", але Aggregate завжди працював так, як очікувалося. Тому я вважаю, що це не рекомендується використовувати String.Join
Manish Jain

Дивно? Коли я прокоментував Перший стоповий годинник, який був для Aggregate; тоді я не отримую жодної "Фатальної помилки: Ліміт використання пам'яті перевищено". Будь ласка, поясніть! Посилання: dotnetfiddle.net/6YyumS
Manish Jain

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 або інше статистичне значення) з колекції.

Тому метод Агрегат є формою безпечної реалізації рекурсивної функції.

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

Синтаксис:

collection.Aggregate(seed, func, resultSelector);
  • seed - початкове значення за замовчуванням;
  • функц - наша рекурсивна функція. Це може бути лямбда-вираз, делегат Func або тип функції TF (T результат, T nextValue);
  • resultSelector - це може бути така функція, як функція, або вираз для обчислення, перетворення, зміни, перетворення кінцевого результату.

Як це працює:

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.

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 замість лямбда-виразу.

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

Вибір з індексом використовується в функції агрегування для підсумовування відповідних стовпців і повернення нового масиву; {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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.