このヘージと関連するページ一覧

  • LINQ - Select
  • LINQ - GroupBy
  • LINQ - Join

  • このページの最終更新日 2020/10/25

    [C#] LINQ - GroupBy

    1. グループ毎に集計する

     このページではLINQを使ってグループ修正を行う方法を学習していきます。具体的な例として、A, B, Cの3つチームがあり、何かしらのゲームを行って各試行に対して得点が与えられるものとします。それぞれのチームでの最高得点は何点かを算出したいとします。code.1にGroupByメソッドを使用して点数を算出するコードを提示します。

    - code.1 -
    public class TeamScore
    {
        public char TeamName { get; set; }
        public int Score { get; set; }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var teamScoreList = new List<TeamScore>();
            teamScoreList.Add(new TeamScore() { TeamName = 'B', Score = 86 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', Score = 53 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', Score = 27 });
            teamScoreList.Add(new TeamScore() { TeamName = 'C', Score = 71 });
            teamScoreList.Add(new TeamScore() { TeamName = 'B', Score = 50 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', Score = 65 });
    
            var teamScoreGrouped = teamScoreList.GroupBy(m => m.TeamName);
            foreach (var oneTeamScoreGrouped in teamScoreGrouped)
            {
                char teamName = oneTeamScoreGrouped.Key;
                int maxScore = oneTeamScoreGrouped.Select(m => m.Score).Max();
                Console.WriteLine(string.Format("チーム名「{0}」の最高得点は「{1}」点です。", teamName, maxScore)); 
                //チーム名「B」の最高得点は「86」点です。
            }   //チーム名「A」の最高得点は「65」点です。
        }       //チーム名「C」の最高得点は「71」点です。
    }

     グループ集計を行うLINQはしばしば使用しますしとても便利です。こういう実装をいちいちif文とfor文で引っ掻き回しているとあっという間にコードが膨らんでいきます。しかし、GroupByメソッドは理解するのが少し難しいメソッドでもあります。code.1を読んで1つ1つの行でどういうことが行われているか理解出来たでしょうか。型推論varの型が何であるかちゃんと言えますでしょうか。良く解らないという人のために、まずは19-25行目について型をvarと省略せずに実際の型を書いてみます。

    - code.2 -
    IEnumerable<IGrouping<char, TeamScore>> teamScoreGrouped = teamScoreList.GroupBy(m => m.TeamName);
    foreach (IGrouping<char, TeamScore> oneTeamScoreGrouped in teamScoreGrouped)
    {
        char teamName = oneTeamScoreGrouped.Key;
        int maxScore = oneTeamScoreGrouped.Select(m => m.Score).Max();
        Console.WriteLine(string.Format("チーム名「{0}」の最高得点は「{1}」点です。", teamName, maxScore));
    }

     まず19行目のGroupByメソッドによってIEnumerable<IGrouping<TKey, TElement>というオブジェクトが返されます。TKeyは何の項目によってグループ化されたかを表しておりこの場合はチーム名(char型)です。TElementはグループ化されたオブジェクトそのものを表しています。IEnumerable<A>オブジェクトからforeachで要素を取り出すと、取り出される中身のオブジェクトはAとなりますから、20行目のforeachで取り出すオブジェクトは当然IGrouping<char, TeamScore>となります。
     このIGrouping<TKey, TElement>というオブジェクトが理解の難しいところです。22行目でこのオブジェクトのKeyプロパティを取得していますが、これはTKeyの部分、すなわち何でグループ化されているかという情報を取得しています。一方23行目のSelectメソッドはTKeyの部分は無関係で、TElementに対して行われる処理です。少しまだ解り辛いかもしれませんのでcode.3のように oneTeamScoreGroup変数がどうなっているのかもう少し詳しく書いてみます。

    - code.3 -
    public class TeamScore
    {
        public char TeamName { get; set; }
        public int Score { get; set; }
    
      public override string ToString()
        {
            return string.Format("■チーム名「{0}」:得点「{1}」", this.TeamName, this.Score);
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var teamScoreList = new List<TeamScore>();
            teamScoreList.Add(new TeamScore() { TeamName = 'B', Score = 86 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', Score = 53 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', Score = 27 });
            teamScoreList.Add(new TeamScore() { TeamName = 'C', Score = 71 });
            teamScoreList.Add(new TeamScore() { TeamName = 'B', Score = 50 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', Score = 65 });
    
            IEnumerable<IGrouping<char, TeamScore>> teamScoreGrouped = teamScoreList.GroupBy(m => m.TeamName);
            foreach (IGrouping<char, TeamScore> oneTeamScoreGrouped in teamScoreGrouped)
            {
                char teamName = oneTeamScoreGrouped.Key;
                int maxScore = oneTeamScoreGrouped.Select(m => m.Score).Max();
                Console.WriteLine(string.Format("チーム名「{0}」の最高得点は「{1}」点です。", teamName, maxScore));
    
              foreach (TeamScore oneTeamScore in oneTeamScoreGrouped)
                {
                    Console.WriteLine(oneTeamScore);
                }
            }
        }                                                                                                        
    }
    //[出力結果]
    //チーム名「B」の最高得点は「86」点です。
    //■チーム名「B」:得点「86」
    //■チーム名「B」:得点「50」
    //チーム名「A」の最高得点は「65」点です。
    //■チーム名「A」:得点「53」
    //■チーム名「A」:得点「27」
    //■チーム名「A」:得点「65」
    //チーム名「C」の最高得点は「71」点です。
    //■チーム名「C」:得点「71」

     よろしいでしょうか。Grouping<TKey, TElement>オブジェクトには複数のTEmementが含まれており、foreach文では複数含まれているTElementが順番に取り出されるようになります。具体的に例えばoneTeamScoreGrouped変数のKeyプロパティが'A'である場合には、oneTeamScoreGrouped変数の中にはTeamNameが'A'である3つのTeamScoreオブジェクトが含まれていることになります。
     このとき、oneTeamScoreGrouped.Select(m => m.Score).Max()はTeamNameが'A'である3つのTeamScoreオブジェクトから、点数項目のみを抽出してその最大値を取得するということになります。


    2. キーが複数の項目の場合

     先ほどはチーム名(TeamName)という1つのプロパティ項目だけでグループ集計を行いましたが、複数の項目でグループ集計したい場合もあったりします。具体的に、チーム名(TeamName)だけでなくチーム番号(TeamNumber)というプロパティも用意して、チーム名とチーム番号が同じ要素について最大得点を求めることにします。
     解決方法は難しくはありません。例えばcode.4のようにTupleを使えばあっさり解決するでしょう。または匿名型を使用しても結果は同じになります。

    - code.4 -
    public class TeamScore
    {
        public char TeamName { get; set; }
        public int TeamNumber { get; set; }
        public int Score { get; set; }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var teamScoreList = new List<TeamScore>();
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 2, Score = 86 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 1, Score = 53 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 1, Score = 27 });
            teamScoreList.Add(new TeamScore() { TeamName = 'B', TeamNumber = 1, Score = 71 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 2, Score = 50 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 1, Score = 65 });
    
          IEnumerable<IGrouping<Tuple<char,int>, TeamScore>> teamScoreGrouped = teamScoreList.GroupBy(m => Tuple.Create(m.TeamName, m.TeamNumber));
            foreach (IGrouping<Tuple<char,int>, TeamScore> oneTeamScoreGrouped in teamScoreGrouped)
            {
                char teamName = oneTeamScoreGrouped.Key.Item1;
                int teamNumber = oneTeamScoreGrouped.Key.Item2;
                int maxScore = oneTeamScoreGrouped.Select(m => m.Score).Max();
    
                Console.WriteLine(string.Format("チーム名「{0}」チーム番号「{1}」の最高得点は「{2}」点です。", teamName, teamNumber, maxScore));
            }
        }
    }
    //[出力結果]
    //チーム名「A」チーム番号「2」の最高得点は「86」点です。
    //チーム名「A」チーム番号「1」の最高得点は「65」点です。
    //チーム名「B」チーム番号「1」の最高得点は「71」点です。

     しかしこれを、code.5のようにチーム名とチーム番号を抽出するクラスを用意してはいけません。結果が違ってきます。このような違いが出る理由はある程度プログラムの勉強が出来ている人なら解るでしょう。Tupleは値型ですが、クラスは参照型であることに起因しています。Tupleの場合は値型なのでTuple内のそれぞれの変数の値が一致していれば同じものだと見なされますが、参照型であるクラスのオブジェクトはクラス内の全てのプロパティの値が等しくても、同じメモリアドレスを参照していなければ異なるオブジェクトであると見なされてしまうのです。
     そのため、GroupByメソッドを実行したときも、異なるオブジェクトは異なるキーであるものとしてグループ化がされてしまいます。

    - code.5 -
    public class TeamScore
    {
        public char TeamName { get; set; }
        public int TeamNumber { get; set; }
        public int Score { get; set; }
    }
    
    public class TeamNameAndTeamNumber
    {
        public char TeamName { get; set; }
        public int TeamNumber { get; set; }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            var teamScoreList = new List<TeamScore>();
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 2, Score = 86 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 1, Score = 53 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 1, Score = 27 });
            teamScoreList.Add(new TeamScore() { TeamName = 'B', TeamNumber = 1, Score = 71 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 2, Score = 50 });
            teamScoreList.Add(new TeamScore() { TeamName = 'A', TeamNumber = 1, Score = 65 });
    
            var teamScoreGrouped = teamScoreList.GroupBy(m => new TeamNameAndTeamNumber(){TeamName = m.TeamName, TeamNumber = m.TeamNumber});
            foreach (var oneTeamScoreGrouped in teamScoreGrouped)
            {
                char teamName = oneTeamScoreGrouped.Key.TeamName;
                int teamNumber = oneTeamScoreGrouped.Key.TeamNumber;
                int maxScore = oneTeamScoreGrouped.Select(m => m.Score).Max();
    
                Console.WriteLine(string.Format("チーム名「{0}」チーム番号「{1}」の最高得点は「{2}」点です。", teamName, teamNumber, maxScore));
            }
        }
    }
    //[出力結果]
    //チーム名「A」チーム番号「2」の最高得点は「86」点です。
    //チーム名「A」チーム番号「1」の最高得点は「53」点です。
    //チーム名「A」チーム番号「1」の最高得点は「27」点です。
    //チーム名「B」チーム番号「1」の最高得点は「71」点です。
    //チーム名「A」チーム番号「2」の最高得点は「50」点です。
    //チーム名「A」チーム番号「1」の最高得点は「65」点です。