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

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

【C#,LINQ】Intersect~配列やリストの積集合が欲しいとき~

 C#のLINQの関数であるIntersect()の使い方についてです。
 配列やリストの要素同士を比べて、積集合のシーケンスを作成することが出来ます。
f:id:urahimono:20180721070251p:plain


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

積集合が欲しいの

 配列やリスト同士を比べて、同じ要素のものだけを残した集合を積集合と呼ぶそうですよ。
共通部分 (数学) - Wikipedia

 これをコード上で実現する場合は、LINQのIntersect()を使いましょう。

public static IEnumerable<TSource> Intersect<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second );
Enumerable.Intersect(TSource) メソッド (IEnumerable(TSource), IEnumerable(TSource)) (System.Linq)

 使い方は簡単。
 引数に比較用の配列やリストなどのシーケンスを指定するだけでOKです。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[] numbersA = new int[] { 1, 2, 3, 4, 5 };
        int[] numbersB = new int[] { 8, 6, 4, 2, 0 };

        IEnumerable<int> results    = numbersA.Intersect( numbersB );

        // 結果発表
        System.Console.WriteLine( "numbersA:{0}", numbersA.Text() );
        System.Console.WriteLine( "numbersB:{0}", numbersB.Text() );
        System.Console.WriteLine( "results :{0}", results.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

numbersA:[1], [2], [3], [4], [5],
numbersB:[8], [6], [4], [2], [0],
results :[2], [4],

 numbersAnumbersBの両方にある、24 だけのシーケンスが作られました。

比較処理を自作するの

 このようなクラスを用意します。

Program.cs

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

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

 このクラスの配列に対して、Intersect()を使用してみましょう。 

Program.cs

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

public static class Program
{
    private class Parameter
    {
        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 )
    {
        // 人物データ
        Parameter[] dataA = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 5, Name = "清次郎" },
            new Parameter() { ID = 3, Name = "誠三郎" },
            new Parameter() { ID = 9, Name = "征史郎" },
        };
        Parameter[] dataB = new Parameter[]
        {
            new Parameter() { ID = 5, Name = "清次郎" },
            new Parameter() { ID = 3, Name = "誠三郎" },
            new Parameter() { ID = 2, Name = "征史郎" },
        };


        IEnumerable<Parameter> results  = dataA.Intersect( dataB );

        // 結果発表
        System.Console.WriteLine( "dataA  :{0}", dataA.Text() );
        System.Console.WriteLine( "dataB  :{0}", dataB.Text() );
        System.Console.WriteLine( "results:{0}", results.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

dataA :[ID:0, Name:正一郎], [ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:9, Name:征史郎],
dataB :[ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:2, Name:征史郎],
results:

 一つも同じものがないという判定が下りました。
 おかしいですね。

 これは、各自がParameterクラスをnewしてインスタンス化していることが問題です。
 この場合は、各自の参照はバラバラになってしまい、プロパティの値は同じでも、違うデータ扱いになっているため、正しく積集合が取れていないのです。

 同じ参照を使うことで解決はできますが、今回は要素が同じかの判定を、参照からではなく、中身のプロパティの値で判断するようにしてみましょう。

 比較用のクラスとして、IEqualityComparerを継承したクラスを作成します。

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

Program.cs

private class ParameterComparer : IEqualityComparer<Parameter>
{
    public bool Equals( Parameter i_lhs, Parameter i_rhs )
    {
        if( i_lhs.ID   == i_rhs.ID &&
            i_lhs.Name == i_rhs.Name )
        {
            return true;
        }
        return false;
    }

    public int GetHashCode( Parameter i_obj )
    {
        // 本当はnullチェックをしなきゃ駄目だよ。
        return i_obj.ID ^ i_obj.Name.GetHashCode();
    }
}

 このクラスをIntersect()に指定することで、独自の比較処理で判定してくれるようになります。

public static IEnumerable<TSource> Intersect<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer );
Enumerable.Intersect(TSource) メソッド (IEnumerable(TSource), IEnumerable(TSource), IEqualityComparer(TSource)) (System.Linq)

Program.cs

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

public static class Program
{
    private class Parameter
    {
        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 ParameterComparer : IEqualityComparer<Parameter>
    {
        public bool Equals( Parameter i_lhs, Parameter i_rhs )
        {
            if( i_lhs.ID   == i_rhs.ID &&
                i_lhs.Name == i_rhs.Name )
            {
                return true;
            }
            return false;
        }

        public int GetHashCode( Parameter i_obj )
        {
            // 本当はnullチェックをしなきゃ駄目だよ。
            return i_obj.ID ^ i_obj.Name.GetHashCode();
        }
    }

    static void Main( string[] args )
    {
        // 人物データ
        Parameter[] dataA = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 5, Name = "清次郎" },
            new Parameter() { ID = 3, Name = "誠三郎" },
            new Parameter() { ID = 9, Name = "征史郎" },
        };
        Parameter[] dataB = new Parameter[]
        {
            new Parameter() { ID = 5, Name = "清次郎" },
            new Parameter() { ID = 3, Name = "誠三郎" },
            new Parameter() { ID = 2, Name = "征史郎" },
        };

        ParameterComparer compare = new ParameterComparer();
        IEnumerable<Parameter> results  = dataA.Intersect( dataB, compare );

        // 結果発表
        System.Console.WriteLine( "dataA  :{0}", dataA.Text() );
        System.Console.WriteLine( "dataB  :{0}", dataB.Text() );
        System.Console.WriteLine( "results:{0}", results.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

dataA :[ID:0, Name:正一郎], [ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:9, Name:征史郎],
dataB :[ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:2, Name:征史郎],
results:[ID:5, Name:清次郎], [ID:3, Name:誠三郎],

 今度は正しく積集合が取れているみたいです。

 このようにIntersect()を七兆回ほど使って、いろいろな集合をとってみてください。

LINQのリンク