徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【C#,LINQ】GroupJoin~配列やリストをグループ化して結合したいとき~

 C#のLINQの関数であるGroupJoin()の使い方についてです。
 複数の配列やリストの要素を結合してグループ化することが出来ます。
f:id:urahimono:20180721073006p:plain


この記事には.NET Framework 4.6.1を使用しています。

グループ化して結合だ

 LINQには、データを結合するJoin()、グループ化するGroupBy() があります。
www.urablog.xyz
www.urablog.xyz

 それをまとめておこないたいときには、GroupJoin()というものがあります。

public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector );
Enumerable.GroupJoin(TOuter, TInner, TKey, TResult) メソッド (IEnumerable(TOuter), IEnumerable(TInner), Func(TOuter, TKey), Func(TInner, TKey), Func(TOuter, IEnumerable(TInner), TResult)) (System.Linq)

 ……引数が多くて、なかなか面倒くさそうな関数ではありますが、一度使ってみましょう。
 ではまず、データ用のクラスを二種類ほど用意してみます。

Program.cs

private class PersonData
{
    public string   Name    { get; set; }
    public int      Age     { get; set; }
    public int      ItemID  { get; set; }

    public override string ToString()
    {
        return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID );
    }
}

Program.cs

private class ItemData
{
    public int      ID      { get; set; }
    public string   Name    { get; set; }

    public override string ToString()
    {
        return string.Format( "ID:{0}, Name:{1}", ID, Name );
    }
}

 PersonDataは人の情報クラス。
 名前とアイテムIDと年齢を持っています。

 ItemDataはアイテムの情報クラス。
 IDと名前を持っています。

 PersonDataの持つアイテムIDと、ItemDataの持つIDは同じものを指しています。

 「ID = 1, Name = "金"」というデータを持つItemDataがあり、
 「ItemID = 1」というデータを持つPersonDataがある場合、
 この「ItemID = 1」は、「ID = 1, Name = "金"」のItemDataを指すことになります。

 このデータを用いてGroupJoin()を使ってみましょう。
 えーと、引数が多いので面倒なのですが、

  • 第一引数:紐づけたい情報の配列やリスト
  • 第二引数:自分自身の紐づけるプロパティの取得処理
  • 第三引数:紐づけたい情報の配列やリストの紐づけるプロパティの取得処理
  • 第四引数:紐づけられた情報に対して行う処理

 を記述する感じです。
 ……うまく説明できている気がしないので、コード見てみてください。

Program.cs

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

public static class Program
{
    private class PersonData
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        public int      ItemID  { get; set; }

        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID );
        }
    }

    private class ItemData
    {
        public int      ID      { get; set; }
        public string   Name    { get; set; }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    private class GroupData
    {
        public string       ItemName    { get; set; }
        public PersonData[] Persons     { get; set; }
    }

    static void Main( string[] args )
    {
        // 人物データ
        PersonData[] persons    = new PersonData[]
        {
            new PersonData() { Name = "正一郎", Age = 35, ItemID = 0 },
            new PersonData() { Name = "清次郎", Age = 27, ItemID = 1 },
            new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3 },
            new PersonData() { Name = "征史郎", Age = 18, ItemID = 0 },
        };

        ItemData[] items = new ItemData[]
        {
            new ItemData() { ID = 0, Name = "金" },
            new ItemData() { ID = 1, Name = "権力" },
            new ItemData() { ID = 2, Name = "人望" },
        };

        // アイテム情報でグループ化するよ。
        IEnumerable<GroupData> groupList = items.GroupJoin(
                                                    persons,
                                                    item => item.ID,
                                                    person => person.ItemID,
                                                    ( item, personList ) => 
                                                        new GroupData() { ItemName = item.Name, Persons = personList.ToArray() } );



        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );
        System.Console.WriteLine( "items  :{0}", items.Text() );

        foreach( var group in groupList )
        {
            System.Console.WriteLine( "item:{0}", group.ItemName );
            foreach( var person in group.Persons )
            {
                System.Console.WriteLine( "\t{0}", person.Name );
            }
        }

        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なコレクションのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Name:正一郎, Age:35, ItemID:0], [Name:清次郎, Age:27, ItemID:1], [Name:誠三郎, Age:27, ItemID:3], [Name:征史郎, Age:18, ItemID:0],
items :[ID:0, Name:金], [ID:1, Name:権力], [ID:2, Name:人望],
item:金
正一郎
征史郎
item:権力
清次郎
item:人望

 アイテムごとに各自分情報をグループ化しています。
 今回はGroupDataクラスというグループ化専用のクラスを用意していますが、ほかで使わないのなら匿名型にしてしまってもいいかもしれません。
 匿名型を使った例が以下の通りです。

Program.cs

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

public static class Program
{
    private class PersonData
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        public int      ItemID  { get; set; }

        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID );
        }
    }

    private class ItemData
    {
        public int      ID      { get; set; }
        public string   Name    { get; set; }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        // 人物データ
        PersonData[] persons    = new PersonData[]
        {
            new PersonData() { Name = "正一郎", Age = 35, ItemID = 0 },
            new PersonData() { Name = "清次郎", Age = 27, ItemID = 1 },
            new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3 },
            new PersonData() { Name = "征史郎", Age = 18, ItemID = 0 },
        };

        ItemData[] items = new ItemData[]
        {
            new ItemData() { ID = 0, Name = "金" },
            new ItemData() { ID = 1, Name = "権力" },
            new ItemData() { ID = 2, Name = "人望" },
        };

        // アイテム情報でグループ化するよ。
        var groupList = items.GroupJoin(
                                    persons,
                                    item => item.ID,
                                    person => person.ItemID,
                                    ( item, personList ) => 
                                        new { ItemName = item.Name, Persons = personList } );



        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );
        System.Console.WriteLine( "items  :{0}", items.Text() );

        foreach( var group in groupList )
        {
            System.Console.WriteLine( "item:{0}", group.ItemName );
            foreach( var person in group.Persons )
            {
                System.Console.WriteLine( "\t{0}", person.Name );
            }
        }

        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なコレクションのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Name:正一郎, Age:35, ItemID:0], [Name:清次郎, Age:27, ItemID:1], [Name:誠三郎, Age:27, ItemID:3], [Name:征史郎, Age:18, ItemID:0],
items :[ID:0, Name:金], [ID:1, Name:権力], [ID:2, Name:人望],
item:金
正一郎
征史郎
item:権力
清次郎
item:人望

グループ化する際の比較処理を指定する

 さて、ここで突然ですが、先ほど使っていたIDを浮動小数型にしてみます。

Program.cs

private class PersonData
{
    public string   Name    { get; set; }
    public int      Age     { get; set; }
    public float    ItemID  { get; set; }

    public override string ToString()
    {
        return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID );
    }
}

private class ItemData
{
    public float    ID      { get; set; }
    public string   Name    { get; set; }

    public override string ToString()
    {
        return string.Format( "ID:{0}, Name:{1}", ID, Name );
    }
}

 そしてこのIDに適当な数値を割り振って、同じようにGroupJoin()を使ってみます。

Program.cs

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

public static class Program
{
    private class PersonData
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        public float    ItemID  { get; set; }

        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID );
        }
    }

    private class ItemData
    {
        public float    ID      { get; set; }
        public string   Name    { get; set; }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        // 人物データ
        PersonData[] persons    = new PersonData[]
        {
            new PersonData() { Name = "正一郎", Age = 35, ItemID = 0.5f },
            new PersonData() { Name = "清次郎", Age = 27, ItemID = 1.2f },
            new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3.3f },
            new PersonData() { Name = "征史郎", Age = 18, ItemID = 0.45f },
        };

        ItemData[] items = new ItemData[]
        {
            new ItemData() { ID = 0.2f, Name = "金" },
            new ItemData() { ID = 1.1f, Name = "権力" },
            new ItemData() { ID = 2.5f, Name = "人望" },
        };

        // アイテム情報でグループ化するよ。
        var groupList = items.GroupJoin(
                                    persons,
                                    item => item.ID,
                                    person => person.ItemID,
                                    ( item, personList ) => 
                                        new { ItemName = item.Name, Persons = personList } );



        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );
        System.Console.WriteLine( "items  :{0}", items.Text() );

        foreach( var group in groupList )
        {
            System.Console.WriteLine( "item:{0}", group.ItemName );
            foreach( var person in group.Persons )
            {
                System.Console.WriteLine( "\t{0}", person.Name );
            }
        }

        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なコレクションのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Name:正一郎, Age:35, ItemID:0.5], [Name:清次郎, Age:27, ItemID:1.2], [Name:誠三郎, Age:27, ItemID:3.3], [Name: 征史郎, Age:18, ItemID:0.45],
items :[ID:0.2, Name:金], [ID:1.1, Name:権力], [ID:2.5, Name:人望],
item:金
item:権力
item:人望

 グループ化がうまくされませんでした。
 似たような数値ではありますが、若干値が違うため、当然の結果ではあります。

 ではここで、ほとんど値が同じものは同じ値扱いになるように比較処理を自作してみましょう。
 IEqualityComparerを継承したクラスに比較処理を記述することでオリジナルの比較処理が作成できます。

IEqualityComparer
IEqualityComparer(T) インターフェイス (System.Collections.Generic)

 こんな感じに作ってみました。

Program.cs

private class GroupComparer : IEqualityComparer<float>
{
    public bool Equals( float i_lhs, float i_rhs )
    {
        return (int)System.Math.Floor( i_lhs ) == (int)System.Math.Floor( i_rhs );
    }

    public int GetHashCode( float i_value )
    {
        int value = (int)System.Math.Floor( i_value );
        return value.GetHashCode();
    }
}

 この比較クラスをGroupJoin()に指定してあげましょう。

public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector, IEqualityComparer<TKey> comparer );
Enumerable.GroupJoin(TOuter, TInner, TKey, TResult) メソッド (IEnumerable(TOuter), IEnumerable(TInner), Func(TOuter, TKey), Func(TInner, TKey), Func(TOuter, IEnumerable(TInner), TResult), IEqualityComparer(TKey)) (System.Linq)

Program.cs

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

public static class Program
{
    private class PersonData
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        public float    ItemID  { get; set; }

        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}, ItemID:{2}", Name, Age, ItemID );
        }
    }

    private class ItemData
    {
        public float    ID      { get; set; }
        public string   Name    { get; set; }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    private class GroupComparer : IEqualityComparer<float>
    {
        public bool Equals( float i_lhs, float i_rhs )
        {
            return (int)System.Math.Floor( i_lhs ) == (int)System.Math.Floor( i_rhs );
        }

        public int GetHashCode( float i_value )
        {
            int value = (int)System.Math.Floor( i_value );
            return value.GetHashCode();
        }
    }

    static void Main( string[] args )
    {
        // 人物データ
        PersonData[] persons    = new PersonData[]
        {
            new PersonData() { Name = "正一郎", Age = 35, ItemID = 0.5f },
            new PersonData() { Name = "清次郎", Age = 27, ItemID = 1.2f },
            new PersonData() { Name = "誠三郎", Age = 27, ItemID = 3.3f },
            new PersonData() { Name = "征史郎", Age = 18, ItemID = 0.45f },
        };

        ItemData[] items = new ItemData[]
        {
            new ItemData() { ID = 0.2f, Name = "金" },
            new ItemData() { ID = 1.1f, Name = "権力" },
            new ItemData() { ID = 2.5f, Name = "人望" },
        };

        // アイテム情報でグループ化するよ。
        GroupComparer compare = new GroupComparer();
        var groupList = items.GroupJoin(
                                    persons,
                                    item => item.ID,
                                    person => person.ItemID,
                                    ( item, personList ) => 
                                        new { ItemName = item.Name, Persons = personList },
                                    compare );



        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );
        System.Console.WriteLine( "items  :{0}", items.Text() );

        foreach( var group in groupList )
        {
            System.Console.WriteLine( "item:{0}", group.ItemName );
            foreach( var person in group.Persons )
            {
                System.Console.WriteLine( "\t{0}", person.Name );
            }
        }

        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なコレクションのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Name:正一郎, Age:35, ItemID:0.5], [Name:清次郎, Age:27, ItemID:1.2], [Name:誠三郎, Age:27, ItemID:3.3], [Name: 征史郎, Age:18, ItemID:0.45],
items :[ID:0.2, Name:金], [ID:1.1, Name:権力], [ID:2.5, Name:人望],
item:金
正一郎
征史郎
item:権力
清次郎
item:人望

 この通り、IDの小数点以下は無視した形でグループ化されるようになりました。
 こんな感じに独自の比較処理でグループ化することもできます。

 GroupJoin()を七兆回ほど使って、いろいろグループ化してみてください。

LINQのリンク