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

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

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

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


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

和集合が欲しいの

 配列やリスト同士を比べて、被らない全ての要素を和集合と呼ぶそうですよ。
和集合 - Wikipedia

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

public static IEnumerable<TSource> Union<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer );
https://msdn.microsoft.com/ja-jp/library/bb341731.aspx

 使い方は簡単。
 引数に比較用の配列やリストなどのシーケンスを指定するだけで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.Union( 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 :[1], [2], [3], [4], [5], [8], [6], [0],

 numbersAnumbersBの両方の要素があり、なおかつ同じ要素が二重になっていないシーケンスが作られました。

比較処理を自作するの

 このようなクラスがあるとします。

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 );
    }
}

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

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.Union( 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:[ID:0, Name:正一郎], [ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:9, Name:征史郎], [ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:2, Name:征史郎],

 同じプロパティの要素が複数入っていますね。
 うまく和集合ができていないみたいです。

 これは、各自が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();
    }
}

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

public static IEnumerable<TSource> Union<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second );
https://msdn.microsoft.com/ja-jp/library/bb358407.aspx

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.Union( 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:0, Name:正一郎], [ID:5, Name:清次郎], [ID:3, Name:誠三郎], [ID:9, Name:征史郎], [ID:2, Name:征史郎],

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

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

LINQのリンク

  • LINQ一覧
    www.urablog.xyz

  • Except
     異なるシーケンス(配列やリスト)の差集合が欲しい!
    www.urablog.xyz

  • Intersect
     異なるシーケンス(配列やリスト)の積集合が欲しい!
    www.urablog.xyz