三流プログラマの戯言

プログラミング初心者が気になったことを書き綴るだけ。主にc#

Enumelable.GroupBy()のオーバーロード

概要

自分でプログラム書いていた時にふと、「これはGroupByでうまく実装できないのか」という疑問が発生して、いろいろ調べた後、
Twitterで「GroupByについてまとめようかなぁ」とつぶやいたところ、「はよ」と脅されたために作成しました。

GroupByのオーバーロードによる違いを軽く解説します。

前提条件/知識

この記事は、脅された自分の忘備録のためであり、詳しい解説はしていません。
使用環境はC#7.1、 . NET Framework 4.7 を基準に書いているので、それ以前のバージョンだと動かない部分があります。
前提知識として、ジェネリックLinq・参照渡し/値渡し・遅延評価などは知っているものとしています。
GroupByの基本的説明はしていない。オーバーロードメソッドのそれぞれの違いを述べているだけです。GroupByの基本動作がわからない人はそれを勉強して読んでください。

GroupByの概要

GroupByメソッド

GroupByは全部で8種類で、型名を省略して書くとそれぞれこんな感じである。

GroupBy(source, keySelector);
GroupBy(source, keySelector, comparer);
GroupBy(source, keySelector, resultSelector);
GroupBy(source, keySelector, elementSelector, resultSelector);
GroupBy(source, keySelector, resultSelector, comparer);
GroupBy(source, keySelector, elementSelector, resultSelector, comparer);
GroupBy(source, keySelector, elementSelector, comparer);
GroupBy(source, keySelector, elementSelector);
引数の型

それぞれの引数の型は以下の通りである。

IEnumerable<TSource> source                              //グループ化するデータ
Func<TSource, TKey> keySelector                          //グループ化のキー
Func<TSource, TElement> elementSelector                  //グループ化の要素のセレクター
Func<TKey, IEnumerable<TSource>, TResult> resultSelector //グループ化の列挙のセレクター
IEqualityComparer<TKey> comparer                         //Keyの比較関数

source , keySelector は知っているものとして解説は省略する,
残りの3つが分かれば、必要に応じたGroupByの使い方ができることになる。

コード例

コード例及び実行例を示す前に、例に使うclass等を先に示しておきます(最初だけusing書いておきます)

使用コード

各学年と各クラスの代表生徒2人をデータとして持っている感じです。

using System;
using System.Collections.Generic;
using System.Linq;

partial class Program
{
    private static Character[] CreateDataBase()
    {
        return new[]
        {
            new Character(new ClassRoom(1, 1), "響"),
            new Character(new ClassRoom(1, 1), "暁"),
            new Character(new ClassRoom(1, 2), "電"),
            new Character(new ClassRoom(1, 2), "雷"),
            new Character(new ClassRoom(1, 3), "夕立"),
            new Character(new ClassRoom(1, 3), "時雨"),
            new Character(new ClassRoom(2, 1), "白露"),
            new Character(new ClassRoom(2, 1), "村雨"),
            new Character(new ClassRoom(2, 2), "島風"),
            new Character(new ClassRoom(2, 2), "大鳳"),
            new Character(new ClassRoom(2, 3), "五月雨"),
            new Character(new ClassRoom(2, 3), "涼風"),
            new Character(new ClassRoom(3, 1), "睦月"),
            new Character(new ClassRoom(3, 1), "如月"),
            new Character(new ClassRoom(3, 2), "弥生"),
            new Character(new ClassRoom(3, 2), "卯月"),
            new Character(new ClassRoom(3, 3), "皐月"),
            new Character(new ClassRoom(3, 3), "水無月"),
        };
    }


    static void ShowResult<T, U>(string header, IEnumerable<IGrouping<T, U>> source)
    {
        Console.WriteLine(header);
        foreach (var group in source)
        {
            Console.WriteLine($"└Key : {group.Key}");
            foreach (var item in group)
            {
                Console.WriteLine($"│└Value : {item}");
            }
        }
        Console.WriteLine();
    }

    static void ShowResult<T>(string header, IEnumerable<IEnumerable<T>> source)
    {
        Console.WriteLine(header);
        foreach (var group in source)
        {
            Console.WriteLine($"└IEnumerable");
            foreach (var item in group)
            {
                Console.WriteLine($"│└Value : {item}");
            }
        }
        Console.WriteLine();
    }

    //教室のデータを格納するクラス
    class ClassRoom
    {
        public ClassRoom(int grade, int classNumber)
        {
            Grade = grade;
            Class = classNumber;
        }

        public int Grade { get; }
        public int Class { get; }

        public override string ToString()
        {
            return $"{Grade}-{Class}";
        }

        public static ClaasRoomComparer Comparer => ClaasRoomComparer.Instance;

        //教室の等しさを確認するためのクラス
        public class ClaasRoomComparer : IEqualityComparer<ClassRoom>
        {
            private static ClaasRoomComparer _instance;

            public static ClaasRoomComparer Instance
                => _instance = _instance ?? new ClaasRoomComparer();


            public bool Equals(ClassRoom x, ClassRoom y)
            {
                return x.Grade == y.Grade && x.Class == y.Class;
            }

            public int GetHashCode(ClassRoom obj)
            {
                return obj.Grade.GetHashCode() ^ obj.Class.GetHashCode();
            }
        }
    }

    //キャラクタのデータを格納するクラス
    class Character
    {
        public Character(ClassRoom room, string name)
        {
            Name = name;
            Room = room;
        }

        public string Name { get; }
        public ClassRoom Room { get; }

        public int Grade => Room.Grade;
        public int Class => Room.Class;

        public override string ToString()
        {
            return $"{Room} : {Name}";
        }

    }
}

※ClassRoomが構造体ではなくクラスなのは意図的です。

comparer

概要
IEqualityComparer<TKey> comparer

これはKeyの比較を行う関数を示します

ソースコード
static void Main(string[] args)
{
    var baseData = CreateDataBase();

    var group0 = baseData.GroupBy(p => p.Room);
    var group1 = baseData.GroupBy(p => p.Room, ClassRoom.Comparer);
    
    ShowResult("group0", group0);
    ShowResult("group1", group1);
}
実行結果

実行結果は以下の通りとなります。(ただし途中以降は省略しています。)

group0
└Key : 1-1
│└Value : 1-1 : 響
└Key : 1-1
│└Value : 1-1 : 暁
└Key : 1-2
│└Value : 1-2 : 電
└Key : 1-2
│└Value : 1-2 : 雷
└Key : 1-3
│└Value : 1-3 : 夕立
└Key : 1-3
│└Value : 1-3 : 時雨
//以下省略

group1
└Key : 1-1
│└Value : 1-1 : 響
│└Value : 1-1 : 暁
└Key : 1-2
│└Value : 1-2 : 電
│└Value : 1-2 : 雷
└Key : 1-3
│└Value : 1-3 : 夕立
│└Value : 1-3 : 時雨
//以下省略
軽い説明

CrassRoomをクラスにしているせいで、普通にGroupByを行うとすべて独立したグループになりますが、きちんと比較子を付けることでそれを回避できます。
この場合では、構造体にすればいいが大きいデータクラスだと有効なのかなと思います。
しかし、別の回避方法がある気がもしますので、あまり使わないと思います。

elementSelector と resultSelector

概要

次に

Func<TSource, TElement> elementSelector 
Func<TKey, IEnumerable<TSource>, TResult> resultSelector 

について。
これらは、Linqでも特に有名なSelect句に近い動きをしてくれます。
elementSelectorはグループ化される各要素に対して、射影を行います。ToDictionaryのelementSelectorと一緒と説明したほうが早いかもしれません。
resultSelector は各グループの要素群に対して何かしらの処理を行ったものをグループの要素として持つものです。このままだとelementSelectorとの違いが分かりにくいが、処理対象が要素ではなく、列挙(≒配列)に対して行えるので選択の幅が広くなります。欠点としては、返り値がIEnumerable<IGruping>ではなくなるため、Kyeの参照ができなくなることです。

ソースコード
static void Main(string[] args)
{
    var baseData = CreateDataBase();

    var group0 = baseData.GroupBy(p => p.Grade, p => p.Name);
    var group1 = 
        baseData.GroupBy(p => p.Grade,  (p, q) => q.OrderByDescending(r => r.Class));

    ShowResult("group0", group0);
    ShowResult("group1", group1);
}
実行結果

実行結果は以下の通り

group0
└Key : 1
│└Value : 響
│└Value : 暁
│└Value : 電
│└Value : 雷
│└Value : 夕立
│└Value : 時雨
└Key : 2
│└Value : 白露
│└Value : 村雨
│└Value : 島風
//以下省略

group1
└IEnumerable
│└Value : 1-3 : 夕立
│└Value : 1-3 : 時雨
│└Value : 1-2 : 電
│└Value : 1-2 : 雷
│└Value : 1-1 : 響
│└Value : 1-1 : 暁
└IEnumerable
│└Value : 2-3 : 五月雨
│└Value : 2-3 : 涼風
│└Value : 2-2 : 島風
│└Value : 2-2 : 大鳳
//以下省略
軽い説明

一応、下記二つは同じ実行結果となります。

var group0 = baseData.GroupBy(p => p.Grade).Select(p=>p.OrderByDescending(r => r.Class));
var group1 = baseData.GroupBy(p => p.Grade, (p, q) => q.OrderByDescending(r => r.Class));

しかし、resultSelectorでは指定した場合Keyの情報も取れるので、無名タプルとかにしておけば、後からDictionaryにすることも可能となります。

ちなみに、GroupByの返り値は

IEnumerable<IGrouping<TKey, TSource>> 
IEnumerable<TResult>

の二つがありますが、resultSelectorを引数に取るメソッドの場合は下になります。

ところで

GroupByの返り値に対して、GetEnumerator()を呼び出すと、Lookupのインスタンスが生成されますが、それならToLookupすればいいんじゃないかなと思ってしまいます。
一応、ToLookupはメソッドを実行した時にインスタンスを生成し、GroupByはGetEnumeratorを実行するまではインスタンス化されないという違いがありますが、果たしてその違いが重要となる場合はあるのでしょうか。