Help us understand the problem. What is going on with this article?

【C#】LINQ備忘録2 ~集計編~

More than 3 years have passed since last update.

【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}");
}

実行結果
1.PNG

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}");
}

実行結果
2.PNG

どうでしょう。従来の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}");
}

実行結果
3.PNG

これも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}");
}

実行結果
4.PNG

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}");
}

実行結果
5.PNG

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}");
}

実行結果
6.PNG

Aggregateは、式に計算式を適用して畳み込みを行います。言葉で書くと結構わかりづらいので、図でイメージを書いてみました。
aa.PNG

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}");
}

実行結果
7.PNG

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

logikuma
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away