うら干物書き

ゲームを作りたい!

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

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


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

差集合が欲しいの

 配列やリスト同士を比べて、同じ要素のものだけを弾いて、残った集合を差集合と呼ぶそうですよ。
差集合 - Wikipedia

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

public static IEnumerable<TSource> Except<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second );
Enumerable.Except(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.Except( 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], [3], [5],

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

比較処理を自作するの

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

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

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

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.Except( 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:征史郎],

 うまく差集合が取れていないように思えます。

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

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

public static IEnumerable<TSource> Except<TSource>( this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer );
Enumerable.Except(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.Except( 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:9, Name:征史郎],

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

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

LINQのリンク

  • LINQ一覧
    www.urablog.xyz

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

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