Aggregate?
Aggregateメソッドとはなんぞや
Microsoft曰く
まずはMSの公式ドキュメントを見てみます。
シーケンスにアキュムレータ関数を適用します。
(中略)
一般的な集計操作を簡略化するために、標準クエリ演算子には、汎用カウントメソッド、 Count 、および4つの数値集計メソッド (、、、および) も含まれて Min Max Sum Average います。
🤔???????
MSの公式ドキュメントは翻訳の質が低いことが多くて困りますね。
概要
"Aggregate"は日本語で「集計」とか大体その辺を意味する言葉です。
その名の通り、IEnumerable<T>
を継承したコレクションの中身全てについて集計処理を行ってくれる汎用性の高いメソッドです。
やろうと思えばLinqの他のメソッド、Min・MaxもSumもAverageも、Aggregateを使えば自力で書けたりするわけですね。
(処理の意味の明確化のためにはそれ用にあつらえてあるメソッドを使うべきですが)
Aggregateの動き方
使い方を知る前に、Aggregateがどのような動きをするのか確認しておきましょう。
なお、サンプルコードはトップレベルステートメントを使っているので、C# 9(.NET 5) 以降のバージョンでしかコンパイルが通らない点に注意してください。
Aggregateメソッドには3つのオーバーロードがあります。
まずは一番基本的な、集計用のラムダ式一つだけを引数としてとるものからはじめます。
using System;
using System.Linq;
var array = new[] { 1, 2, 3, 4, 5 };
var sum = array.Aggregate((result, current) => result + current);
Console.WriteLine($"1~5 の総和は{sum}です!"); // 1~5 の総和は15です!
Aggregateに渡す集計用ラムダ式には2つの引数が必要です。
上記の例ではresult
とcurrent
と名付けています。
それではAggregateメソッドを実行したときの動きを追ってみましょう。
- 初回は
result
に最初の要素が、current
には次の要素が代入される。
この例だとresult = 1
current = 2
-
result + current
が評価され、結果がreturn
される。
例では3
が返ります。 - 返り値が
result
に新たに代入され、current
に次の要素が入る。
result = 3
current = 3
- 同様に最後の要素まで計算する。
つまり、Aggregateにわたすラムダ式の構成要素は以下のようなものと言えるわけです:
- 第一引数には直前の評価の戻り値が入っていて、次の評価に値を伝えてくれる。
- 第ニ引数には現在の要素が次々と入っていく。
- ラムダ式の戻り値が次に持ち越される計算結果になる。
このことがわかれば、あとの2つのオーバーロードの理解も簡単です。単に処理の前後に何かしらの処理をくっつけているだけなので。
using System;
using System.Linq;
var array = new[] { 1, 2, 3, 4, 5 };
// 第一引数として値や変数を渡すことで、第二引数のラムダ式の最初の引数 result の初期値を決められます。
// 初期値を与えたとき、最初の current はコレクションの最初の要素になります。
var sum1 = array.Aggregate(15, (result, current) => result + current);
Console.WriteLine($"15から始めた総和は{sum1}です!"); // 15から始めた総和は30です!
// 第三引数としてラムダ式を渡すことで、計算結果に対して何かしらの処理を施すことができます。
var sum2 = array.Aggregate(0, (result, current) => result + current, result => result * result);
Console.WriteLine($"1~5 の総和の2乗は{sum2}です!"); // 1~5 の総和の2乗は225です!
便利な使い方
Aggregateの動き方がわかったところで、ようやく本題の具体的にはどんな活用方法があるかを紹介していきます。
複数要素の同時集計
Linqに用意されているSumやAverage、Min・Maxメソッドは同時に一つの値についてしか計算できません。
リストの中に入っているオブジェクトの複数のプロパティについてこれらの結果を求めたい場合、Aggregateを使って自分で書いてやる必要があります。
using System;
using System.Linq;
using System.Drawing;
var array = new[] {
new Point(1, 4),
new Point(3, 2),
new Point(2, 8),
new Point(4, 6)
};
var sum = array.Aggregate((result, current) => new Point(result.X + current.X, result.Y + current.Y));
Console.WriteLine(sum); // {X=10,Y=20}
var max = array.Aggregate((result, current) =>
new Point(
Math.Max(result.X, current.X),
Math.Max(result.Y, current.Y)
));
Console.WriteLine(max); // {X=4,Y=8}
初回投稿時は知らなかった Aggregate を使わずともできた処理たち
[2022/5/6 追記]
この記事を最初に書いたときには知らなかったのですが、最初に書いていた3つの使い方の内、2つは専用のメソッドが用意されていたのを後々知りました。Aggregate が必要になるような場面というのは実際のところかなり少ないのかもしれません。
記事の趣旨とは外れてしまいますが、一度悪い方法を紹介してしまった以上ちゃんとした方法を紹介する責任もあると思ったので、削除ではなく書き換えと紹介という形を取ろうと思います。初回投稿時に書いていた Aggregate での実装は折りたたみの中にしまってあります。比べてみるとAggregate は乱用しないほうがいいことがわかるかと思います。
文字列の連結
要素を文字列化して任意のセパレータを挟んだ文字列に変換します。
var array = new[] { 1, 2, 3, 4, 5 };
var csvRow = string.Join(", ", array);
Console.WriteLine(csvRow);
// 出力
// 1, 2, 3, 4, 5
var matrix = Enumerable.Repeat(csvRow, 4);
var csvMat = string.Join(Environment.NewLine, matrix);
Console.WriteLine(csvMat);
// 出力
// 1, 2, 3, 4, 5
// 1, 2, 3, 4, 5
// 1, 2, 3, 4, 5
// 1, 2, 3, 4, 5
Aggregate による実装
using System;
using System.Linq;
var array = new[] { 1, 2, 3, 4, 5 };
// 要素の加工はSelectによって予め済ましておくほうが、Aggregateに渡すラムダ式を簡潔にできます。
var csvRow = array.Select(i => i.ToString()).Aggregate((result, current) => $"{result}, {current}");
Console.WriteLine(csvRow);
// 出力
// 1, 2, 3, 4, 5
var matrix = Enumerable.Repeat(csvRow, 4);
var csvMat = matrix.Aggregate((result, current) => result + Environment.NewLine + current);
Console.WriteLine(csvMat);
// 出力
// 1, 2, 3, 4, 5
// 1, 2, 3, 4, 5
// 1, 2, 3, 4, 5
// 1, 2, 3, 4, 5
複数の条件を満たしているかの調査
条件をまとめた配列があれば、真理値の総計を取ることでそれらを満たしているかの調査が1行で書けてしまいます。
var conditions = new Func<int, bool>[] {
(value) => 0 < value,
(value) => value < 10,
(value) => value % 2 == 0
};
// すべての条件を満たしているか?
var x = 3;
var isSatisfy = conditions.All(condition => condition(x));
Console.WriteLine(isSatisfy); // False
var y = 4;
isSatisfy = conditions.All(condition => condition(y));
Console.WriteLine(isSatisfy); // True
// 少なくとも一つの条件を満たしているか?
// Any は引数なしだと要素が存在するかを返すが、ラムダ式を引数として取るオーバロードはラムダ式の戻り値の論理和を返す
var z = -1;
isSatisfy = conditions.Any(condition => condition(z));
Console.WriteLine(isSatisfy); // True
Aggregate による実装
using System;
using System.Linq;
var conditions = new Func<int, bool>[] {
(value) => 0 < value,
(value) => value < 10,
(value) => value % 2 == 0
};
// すべての条件を満たしているか?
// 論理積計算のときは初期値を true にします。
var x = 3;
var isSatisfy = conditions.Aggregate(true, (result, current) => result && current(x));
Console.WriteLine(isSatisfy); // False
var y = 4;
isSatisfy = conditions.Aggregate(true, (result, current) => result && current(y));
Console.WriteLine(isSatisfy); // True
// 少なくとも一つの条件を満たしているか?
// 論理和計算のときは初期値を false にします。
var z = -1;
isSatisfy = conditions.Aggregate(false, (result, current) => result || current(z));
Console.WriteLine(isSatisfy); // True
おわり
紹介する使い方は以上でおわりですが、Aggregateメソッドは汎用性の高いメソッドなので、この他にもアイデア次第でいろんな事ができるでしょう。これは、あまり乱用すべきではないということを意味しています。
Linq が for/foreach 文よりも推奨される理由としてよく挙げられるのが、宣言的に反復処理を書けることです。for/foreach は反復処理という制御構文であることを示すのみで、実際の動作はループ文の中身を読まなくてはなりません。
それに対し、 Linq を使えば Where や Select などメソッド名によって「何をしているのか」が明確に記述されることになるため、ループの中身を見なくても処理の概要をつかむことが出来るようになるのでした。
Aggregate は汎用的なメソッドで「畳み込み(fold)を行う」くらいの意味しか持ちません。その畳み込みによって実際に何をしているのかを知るには、中のラムダ式を読まなければなりません。つまり、宣言的に反復処理を書けるという利点を持たないのです。
for/foreach 文のほうが慣れている人も多いと考えられる以上、Aggregate の使用は「なぜここでは for/foreach ではなく Aggregate を使うのか」を自分の中で明確に説明できるような場合に限ったほうが良いかもしれません。