うら干物書き

ゲームを作りたい!

【C#,LINQ】Join~別のデータの配列やリストと結合(内部結合)したいとき~

 C#のLINQの関数であるJoin()の使い方についてです。
 別の情報リストとデータを結合することが出来ます。
f:id:urahimono:20180721064412p:plain


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

データを結合するぞ

 LINQJoin()を使うことでデータを結合することができます。
 ……と言われても、結合ってなんのことさ!

 ではJoin()の結合を学ぶために、二つのクラスを作ってみました。

Program.cs

private class PersonData
{
    public string   Name    { get; set; }
    public int      ItemID  { get; set; }
    
    public override string ToString()
    {
        return string.Format( "Name:{0}, ItemID:{1}", Name, 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は同じものを指しています。

 例えば、ItemDataのIDが[3]の名前が玉子焼きだった場合、
 アイテムID[3]を持つ人は、玉子焼きを持っていることになります。

 PersonDataが配列でいっぱいあり、そしてItemDataも配列でいっぱいあるとき、
 各PersonDataのアイテムIDとItemDataのIDをいい感じに紐づけたいとき。

 そんなときにJoin()を使えばいい感じにしてくれるのです。

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector );
https://msdn.microsoft.com/ja-jp/library/bb534675.aspx

 Join()は引数が多いので、ちと面倒くさいです。
 第一引数に、紐づけたい情報の配列やリストを。
 第二引数に、自分自身の紐づけるプロパティの取得処理を。
 第三引数に、紐づけたい情報の配列やリストの紐づけるプロパティの取得処理を。
 第四引数に、紐づけられた情報に対して行う処理を。

 かけばOKです。
 多分、うまく説明できていない気がするので、コードをみて確認してみてください。

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      ItemID  { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, ItemID:{1}", Name, 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 JoinData
    {
        public PersonData   Person  { get; set; }
        public ItemData     Item    { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Person:{{ {0} }}, Item:{{ {1} }}", Person, Item );
        }
    }


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

        // アイテムデータ
        ItemData[] items = new ItemData[]
        {
            new ItemData() { ID = 0, Name = "金" },
            new ItemData() { ID = 1, Name = "権力" },
        };

        // 人物データのItemIDとアイテムデータのIDが同じものを結合する。
        IEnumerable<JoinData> joinList = persons.Join( 
                                                    items,
                                                    person => person.ItemID,
                                                    item => item.ID,
                                                    ( person, item ) => new JoinData() { Person = person, Item = item } );

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

        // 入力待ち用
        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:正一郎, ItemID:0], [Name:清次郎, ItemID:1], [Name:誠三郎, ItemID:2], [Name:征史郎, ItemID:0],
items :[ID:0, Name:金], [ID:1, Name:権力],
join :[Person:{ Name:正一郎, ItemID:0 }, Item:{ ID:0, Name:金 }], [Person:{ Name:清次郎, ItemID:1 }, Item:{ ID:1, Name:権力 }], [Person:{ Name:征史郎, ItemID:0 }, Item:{ ID:0, Name:金 }],

 上記のコードでは、紐づけた情報をJoinDataクラスにまとめています。
 ですが、これぐらいの情報量なら匿名型で返してしまったほうが楽かもしれません。

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      ItemID  { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, ItemID:{1}", Name, 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 = "正一郎", ItemID = 0 },
            new PersonData() { Name = "清次郎", ItemID = 1 },
            new PersonData() { Name = "誠三郎", ItemID = 2 },
            new PersonData() { Name = "征史郎", ItemID = 0 },
        };

        // アイテムデータ
        ItemData[] items = new ItemData[]
        {
            new ItemData() { ID = 0, Name = "金" },
            new ItemData() { ID = 1, Name = "権力" },
        };

        // 人物データのItemIDとアイテムデータのIDが同じものを結合する。
        var joinList =  persons.Join( 
                                    items,
                                    person => person.ItemID,
                                    item => item.ID,
                                    ( person, item ) => new { PersonName = person.Name, ItemName = item.Name } );

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

        {
            string text = string.Empty;
            foreach( var value in joinList )
            {
                text += string.Format( "[Person:{0}, Item:{1}], ", value.PersonName, value.ItemName );
            }
            System.Console.WriteLine( "join   :{0}", text );
        }
        
        // 入力待ち用
        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:正一郎, ItemID:0], [Name:清次郎, ItemID:1], [Name:誠三郎, ItemID:2], [Name:征史郎, ItemID:0],
items :[ID:0, Name:金], [ID:1, Name:権力],
join :[Person:正一郎, Item:金], [Person:清次郎, Item:権力], [Person:征史郎, Item:金],

比較処理を指定する時

 さてここで、先ほど使ったPersonDataItemDataを変えてみます。

Program.cs

private class PersonData
{
    public string   Name    { get; set; }
    public ItemData Item    { get; set; }
    
    public override string ToString()
    {
        return string.Format( "Name:{0}, Item:{1}", Name, Item );
    }
}

Program.cs

private class ItemData
{
    public string   Name    { get; set; }
    
    public override string ToString()
    {
        return string.Format( "{0}", Name );
    }
}

 各自IDをなくし、PersonDataが直接ItemDataを持つ形に変更しました。
 この状態で、同じItemDataを持つPersonDataを、相方として結合する処理を書いてみましょう。

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 ItemData Item    { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, Item:{1}", Name, Item );
        }
    }

    private class ItemData
    {
        public string   Name    { get; set; }
        
        public override string ToString()
        {
            return string.Format( "{0}", Name );
        }
    }

    static void Main( string[] args )
    {
        // 人物データ
        PersonData[] persons = new PersonData[]
        {
            new PersonData() { Name = "正一郎", Item = new ItemData() { Name = "金" } },
            new PersonData() { Name = "清次郎", Item = new ItemData() { Name = "権力" } },
            new PersonData() { Name = "誠三郎", Item = new ItemData() { Name = "人望" } },
            new PersonData() { Name = "征史郎", Item = new ItemData() { Name = "金" } },
        };

        // 相方データ
        PersonData[] partners = new PersonData[]
        {
            new PersonData() { Name = "A", Item = new ItemData() { Name = "権力" } },
            new PersonData() { Name = "B", Item = new ItemData() { Name = "金" } },
            new PersonData() { Name = "C", Item = new ItemData() { Name = "熱き正義の心" } },
        };

        // 人物データと相方データで同じItemを持っているものを結合する。
        var joinList =  persons.Join(
                                    partners,
                                    person => person.Item,
                                    partner => partner.Item,
                                    ( person, partner ) => new { PersonName = person.Name, PartnerName = partner.Name } );

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

        {
            string text = string.Empty;
            foreach( var value in joinList )
            {
                text += string.Format( "[Person:{0}, `Partner:{1}], ", value.PersonName, value.PartnerName );
            }
            System.Console.WriteLine( "join    :{0}", text );
        }
        
        // 入力待ち用
        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:正一郎, Item:金], [Name:清次郎, Item:権力], [Name:誠三郎, Item:人望], [Name:征史郎, Item:金],
partners:[Name:A, Item:権力], [Name:B, Item:金], [Name:C, Item:熱き正義の心],
join :

 結合したものが一つもない、という結果が返ってきました。
 あまりにもダメなマッチングサイトです。

 もちろんこれはマッチングサイトが悪いわけではなく、コードが悪いのです。

 ItemDataはクラスなので参照型です。
 そして、PersonData作成時には、各自newを使ってインスタンス化しています。
 これでは、中身のプロパティが同じ内容では、全て別のインスタンスになっているので、同じ要素扱いにはなりません。
 
 各自newするのではなく、別でインスタンス化したデータを使うことで解決できるのですが、今回はあくまで中身が同じなら同じ要素扱いになるように、比較処理を自作してみましょう。

 比較処理はIEqualityComparerを継承すれば作成することができます。

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

Program.cs

// ItemData比較用クラス
private class ItemComparer : IEqualityComparer<ItemData>
{
    public bool Equals( ItemData i_lhs, ItemData i_rhs )
    {
        // Nameの文字列が同じどうかで判断する。
        return i_lhs.Name == i_lhs.Name;
    }

    public int GetHashCode( ItemData i_item )
    {
        // 本当はnullチェックしなきゃダメだよ……。
        return i_item.Name.GetHashCode();
    }
}

 この比較クラスをJoinクラスに指定してあげればOKです。

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer );
https://msdn.microsoft.com/ja-jp/library/bb549267.aspx

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 ItemData Item    { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, Item:{1}", Name, Item );
        }
    }

    private class ItemData
    {
        public string   Name    { get; set; }
        
        public override string ToString()
        {
            return string.Format( "{0}", Name );
        }
    }

    // ItemData比較用クラス
    private class ItemComparer : IEqualityComparer<ItemData>
    {
        public bool Equals( ItemData i_lhs, ItemData i_rhs )
        {
            // Nameの文字列が同じどうかで判断する。
            return i_lhs.Name == i_lhs.Name;
        }

        public int GetHashCode( ItemData i_item )
        {
            // 本当はnullチェックしなきゃダメだよ……。
            return i_item.Name.GetHashCode();
        }
    }

    static void Main( string[] args )
    {
        // 人物データ
        PersonData[] persons = new PersonData[]
        {
            new PersonData() { Name = "正一郎", Item = new ItemData() { Name = "金" } },
            new PersonData() { Name = "清次郎", Item = new ItemData() { Name = "権力" } },
            new PersonData() { Name = "誠三郎", Item = new ItemData() { Name = "人望" } },
            new PersonData() { Name = "征史郎", Item = new ItemData() { Name = "金" } },
        };

        // 相方データ
        PersonData[] partners = new PersonData[]
        {
            new PersonData() { Name = "A", Item = new ItemData() { Name = "権力" } },
            new PersonData() { Name = "B", Item = new ItemData() { Name = "金" } },
            new PersonData() { Name = "C", Item = new ItemData() { Name = "熱き正義の心" } },
        };

        // 人物データと相方データで同じItemを持っているものを結合する。
        ItemComparer compare = new ItemComparer();
        var joinList =  persons.Join(
                                    partners,
                                    person => person.Item,
                                    partner => partner.Item,
                                    ( person, partner ) => new { PersonName = person.Name, PartnerName = partner.Name },
                                    compare );

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

        {
            string text = string.Empty;
            foreach( var value in joinList )
            {
                text += string.Format( "[Person:{0}, `Partner:{1}], ", value.PersonName, value.PartnerName );
            }
            System.Console.WriteLine( "join    :{0}", text );
        }
        
        // 入力待ち用
        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:正一郎, Item:金], [Name:清次郎, Item:権力], [Name:誠三郎, Item:人望], [Name:征史郎, Item:金],
partners:[Name:A, Item:権力], [Name:B, Item:金], [Name:C, Item:熱き正義の心],
join :[Person:正一郎, Partner:B], [Person:清次郎,Partner:A], [Person:征史郎, `Partner:B],

 こんどはきちんと同じ志を持つもの同士で結合することができました。

 こんな感じにJoin()を使って、七兆組ほどカップリングしてあげてください。

LINQのリンク