【C#】LINQ備忘録2 ~集計編~
はじめに
【C#】LINQ備忘録1 ~初級編~を投稿して、約10か月経ちました。
勿論、まだまだ勉強不足のところはありますが、業務や趣味で遊んでいく中で10か月前よりもC#が使えるようになり、やっとC#erとしてのスタートラインに立てた感じがします。ほんと、C#最高っす。
今回は、LINQの__集計メソッド__を使って計算を行う方法を備忘録的に残したいと思います。今回も前回と同様に、foreach・forを使って求める方法と、LINQで計算を行う方法を2通り紹介していきます。
今回のサンプルソースはGitHubに公開しています。
https://github.com/logikuma1024/LinqRemind2
集計メソッドって?
集計とは?
寄せ集めたいくつかの数を合計すること。また,合計した数。(Weblioより)
集計メソッドは、要素集合の中の値を計算し、計算結果を返します。
LINQの集計メソッドには下記があり、合計だけではなく、それぞれ特定の計算結果を返すことが出来ます。
集計メソッドの一覧
メソッド名 | 求められる計算結果 |
---|---|
Sum | 要素の合計 |
Count | 要素の個数 |
Average | 要素の平均 |
Max | 要素の最大値 |
Min | 要素の最小値 |
Aggregate | 指定した式での集計結果 |
それぞれSQLの集約関数ライクなメソッドになっており、求められる計算結果を集計して返すことが出来ます。列挙の中の値の計算がしたいことは非常に多いので、活躍機会のとっても多いメソッド群です。
各メソッドのサンプル
データサンプル
今回は例が集計メソッドということもあり、下記のようなデータソースを用意し、これに対してクエリを行っていきます。
/// <summary>
/// テストデータ
/// </summary>
private static List<Person> members = new List<Person>
{
new Person {Name = "Aさん", Sex = "男", Age = 20, Score = 80 },
new Person {Name = "Bさん", Sex = "女", Age = 34, Score = 70 },
new Person {Name = "Cさん", Sex = "男", Age = 58, Score = 61 },
new Person {Name = "Dさん", Sex = "男", Age = 29, Score = 50 },
new Person {Name = "Eさん", Sex = "女", Age = 18, Score = 98 }
};
表にすると、下記のような形になります。
Name(名前) | Sex(性別) | Age(年齢) | Score(点数) |
---|---|---|---|
Aさん | 男 | 20 | 80 |
Bさん | 女 | 34 | 70 |
Cさん | 男 | 58 | 61 |
Dさん | 男 | 29 | 50 |
Eさん | 女 | 18 | 98 |
Sum
Sumは、シーケンス内の要素の合計を返します。
/// <summary>
/// Sum関数のテスト
/// </summary>
static void SumTest()
{
//<Scoreの合計を抽出します>
//foreach版-----------------------------------------------
//結果を格納する変数を作成する。
int ret1 = 0;
foreach (var m in members)
//値を足していく。
ret1 += m.Score;
//Linq版--------------------------------------------------
//メンバー表の中からスコアだけを射影し、合計する。
var ret2 = members.Select(x => x.Score).Sum();
//表示する------------------------------------------------
Console.WriteLine($"foreach:{ret1}, Linq:{ret2}");
}
SUMは、値を合計することが出来ます。今回の例では合計したい部分が表の中の「Score」列だけなので、その部分を抜き出し、Sum()メソッドで集計しています。foreach版と比べ、やりたいことに日本語的に即しているので、可読性が高いように感じます。
このコードは更に短縮できます。Sum()メソッドにはFunc<TSource,hoge>を指定するオーバーロードがあり、Selectの中で行っている射影処理をSum(x => x)として記述することが出来ます
//Linq版--------------------------------------------------
//メンバー表の中からスコアの合計を求めます。
var ret2 = members.Sum(x => x.Score);
こうすると、かなりスッキリ集計処理を行うことができますね。集合の中の欲しい部分のみを指定すればよいので、考え方も難しくなく、使いやすいメソッドです。
Count
Countは要素の個数を返します。
/// <summary>
/// Count関数のテスト
/// </summary>
static void CountTest()
{
//<男の数をカウントします>
//foreach版-----------------------------------------------
//結果を格納する変数を作成する。
int ret1 = 0;
foreach(var m in members)
{
if (m.Sex == "男")
//男性の場合はカウントアップ
ret1++;
}
//Linq版--------------------------------------------------
//membersの中で性別が男性の個数を返します。
var ret2 = members.Count(x => x.Sex == "男");
//表示する------------------------------------------------
Console.WriteLine($"foreach:{ret1}, Linq:{ret2}");
}
どうでしょう。従来のforeachを用いた書き方ではどうしてもループにif文が挟まり、可読性の低いコードになっていましたが、Linq版は1行でとってもスマートになります。Sumの時にSelectを省略出来たのと同様に、Countの時はWhereを省略できます。実際に実務でもよく使うコードだけに、基本は押さえておきたいです。
Average
Averageは、要素の平均値を計算します。
/// <summary>
/// Average関数のテスト
/// </summary>
static void AvarageTest()
{
//<Ageの平均値を集計します。>
//foreach版-----------------------------------------------
//計算用の合計値
double sum = 0;
//計算用の件数
int count = 0;
foreach(var m in members)
{
//合計値と件数をカウントアップ
sum += m.Age;
count++;
}
//平均値を算出する
double ret1 = sum / count;
//Linq版--------------------------------------------------
//membersの年齢の平均値を算出する
var ret2 = members.Average(x => x.Age);
//表示する------------------------------------------------
Console.WriteLine($"foreach:{ret1}, Linq:{ret2}");
}
これもCountと同様、かなりコードがきれいになります。どうしても計算が絡むとコードが複雑になり、計算式の間違いからミスが生まれたりもしますが、Linqを用いて算出すると確実です。また、count変数の初期値が0なのは、除算を行うのにかなり危険な気がします。使いましょう、Average。
Max
Maxは、シーケンス内の最大値を抽出します。
/// <summary>
/// Max関数のテスト
/// </summary>
static void MaxTest()
{
//<Scoreの最大値を集計します。>
//foreach版-----------------------------------------------
//最大値を格納する変数
int ret1 = 0;
foreach(var mem in members)
{
if (mem.Score > ret1)
//ret1の値を超える値が出てきた場合は値を更新
ret1 = mem.Score;
}
//Linq版--------------------------------------------------
//membersの中でScoreが最大のものを抽出
var ret2 = members.Max(x => x.Score);
//表示する------------------------------------------------
Console.WriteLine($"foreach:{ret1}, Linq:{ret2}");
}
foreach版のretの扱いが難しく、何度か想定しない結果が返ってきました。
- 不等号を扱って判定しなくてはならない点
- ret1変数が計算過程・計算結果の両方を表している点
で、foreach版のコードとしての可読性はやはり高くありません。ここで抽出するものは「最大値」なのですから、Linqで用意されているMaxメソッドを用いた方が、日本語的にもキレイに見えます。
Min
Minは、シーケンスの中の最小値を抽出します。
/// <summary>
/// Min関数のテスト
/// </summary>
static void MinTest()
{
//<Scoreの最小値を集計します。>
//foreach版-----------------------------------------------
//最小値を格納する変数に、int型の上限値を入れて初期化
int ret1 = int.MaxValue;
foreach (var mem in members)
{
//ret1の値よりも小さい値が出てきた場合は値を更新
if (mem.Score < ret1)
ret1 = mem.Score;
}
//Linq版--------------------------------------------------
//membersの中でスコアが最小のものを抽出
var ret2 = members.Min(x => x.Score);
//表示する------------------------------------------------
Console.WriteLine($"foreach:{ret1}, Linq:{ret2}");
}
Maxと同様に、こちらは最小値を求められるメソッドです。foreach版ではいわゆる「HighValue」を定義する必要があったのに対し、Linq版では意識する必要がありません。
Aggregate
Aggregateは、シーケンスの各要素に式を適用し、計算結果を求めます。
/// <summary>
/// Aggregate関数のテスト
/// </summary>
static void AggregateTest()
{
//<Scoreの積算を取得する>
//foreach版-----------------------------------------------
//積算合計を保持する変数
int ret1 = 1;
foreach (var mem in members)
//値を積算していく。
ret1 = ret1 * mem.Score;
//Linq版--------------------------------------------------
//memberのScoreを順次積算していく。
var ret2 = members.Aggregate(1,(n, next) => n * next.Score);
//表示する------------------------------------------------
Console.WriteLine($"foreach:{ret1}, Linq:{ret2}");
}
Aggregateは、式に計算式を適用して__畳み込み__を行います。言葉で書くと結構わかりづらいので、図でイメージを書いてみました。
Aggregate関数は、元となるシーケンスに順次計算式を適用して、シーケンスの最後まで計算を実行、作成した値を返却します。シード値は計算の一番先頭に入れる集計結果のベースとなる値、計算式は各ステップで実行する計算式です。例ではAggregateによって1,〇,△,□を順次積算していって、最終的な答えを出しています。
Aggregateは式をカスタマイズしてやれば様々な計算が出来ます。Sum・Count・Max・Min・Averageではカバーしきれないものについて、良い感じに使ってみたいです!
おまけ
Aggregateのオーバーロードを使って、計算結果をさらに処理してみます。
/// <summary>
/// Aggregate関数のテスト2
/// </summary>
static void AggregateTest2()
{
//<最高スコアの人を名前付きで出力する>
//for版-----------------------------------------------
//最高スコア
int maxScore = 0;
//リスト検索用インデックス
int index = 0;
for(int i = 0; i < members.Count ; i++)
{
//MAXを求める。
if (members[i].Score > maxScore)
{
//最大値が見つかったら更新し、インデックスを保持。
maxScore = members[i].Score;
index = i;
}
}
string ret1 = $"最高点 : {maxScore}点 / { members[index].Name }";
//Linq版--------------------------------------------------
//membersの中からスコアが最大のものを取得し、名前と一緒に表示する
var ret2 = members.Aggregate(members.First(),
//スコア最大を求める
(max, next) => max.Score < next.Score ? next : max,
//表示結果を作成する
x => $"最高点 : {x.Score}点 / {x.Name }");
//表示する
Console.WriteLine($"foreach:{ret1}\nLinq:{ret2}");
}
Aggregateメソッドのオーバーロードを用いて、処理した結果を射影し、出力結果を作成しています。Person自体をAggregateしているので、シードはmembers.First()として集計しています。今回の例のように、オブジェクト(今回はPerson)のプロパティを計算する場合はforで回すとindexの管理が必要になり、煩雑でした。Aggregateを用いるとスッキリと纏めて書くことができるので、とても素敵です!
まとめ
昨年からC#を使い始め、そろそろC#erとして1歳になります。.NET CoreやWPF等色々触ってきましたがどれも魅力的で、覚えることが沢山ありとても楽しいです。今後もC#について色々学んでみたいと思います。
C#ってステキ!
参考
MSDN
https://msdn.microsoft.com/ja-jp/library/bb896340(v=vs.110).aspx
[C#] Aggregateを使う[LINQ]
https://chomado.com/programming/c-sharp/c-aggregate%E3%82%92%E4%BD%BF%E3%81%86linq/
C# やるなら LINQ を使おう
http://yohshiy.blog.fc2.com/blog-entry-274.html