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

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

【C#,LINQ】SequenceEqual~配列やリストの中身が同じかを調べたいとき~

 C#のLINQの関数であるSequenceEqual()の使い方についてです。
 配列やリスト同士の中身に、同じ要素が入っているかを判定することが出来ます。
f:id:urahimono:20180625231955p:plain


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

要素は同じかな?

 この配列とリストの中身は同じだろうか。
 そんなことを調べるときにはSequenceEqual()です。
 これを使えば、中身が同じかを調べることができます。

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

 ですがSequenceEqual()は、要素が値型参照型かによって挙動が変わってきます。

値型の場合

 要素が値型ならば特に問題はありません。
 以下のように思う存分に使ってみてください。
 配列とリストを比べることだって出来ます。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数値データ
        int[]       dataA   = new int[]     { 0, 1, 2, 3, 4, 5 };
        int[]       dataB   = new int[]     { 0, 1, 2, 4, 3, 5 };
        List<int>   dataC   = new List<int> { 0, 1, 2, 3, 4, 5 };

        // 要素は同じかな?
        bool resultAB = dataA.SequenceEqual( dataB );
        bool resultAC = dataA.SequenceEqual( dataC );
        bool resultBC = dataB.SequenceEqual( dataC );

        // 結果発表
        System.Console.WriteLine( "dataA:{0}", dataA.Text() );
        System.Console.WriteLine( "dataB:{0}", dataB.Text() );
        System.Console.WriteLine( "dataC:{0}", dataC.Text() );

        System.Console.WriteLine( "A と B は同じですか:{0}", resultAB );
        System.Console.WriteLine( "A と C は同じですか:{0}", resultAC );
        System.Console.WriteLine( "B と C は同じですか:{0}", resultBC );
        // 入力待ち用
        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:[0], [1], [2], [3], [4], [5],
dataB:[0], [1], [2], [4], [3], [5],
dataC:[0], [1], [2], [3], [4], [5],
A と B は同じですか:False
A と C は同じですか:True
B と C は同じですか:False

参照型の場合

 問題の要素が参照型の場合です。
 以下のコードを見てみましょう。

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 )
    {
        // 名前とIDのデータ
        Parameter[] dataA = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 5, Name = "清次郎" },
        };
        Parameter[] dataB = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 5, Name = "清次郎" },
        };
        Parameter[] dataC = new Parameter[]
        {
            new Parameter() { ID = 4, Name = "征史郎" },
        };

        // 要素は同じかな?
        bool resultAB = dataA.SequenceEqual( dataB );
        bool resultAC = dataA.SequenceEqual( dataC );
        bool resultBC = dataB.SequenceEqual( dataC );

        // 結果発表
        System.Console.WriteLine( "dataA:{0}", dataA.Text() );
        System.Console.WriteLine( "dataB:{0}", dataB.Text() );
        System.Console.WriteLine( "dataC:{0}", dataC.Text() );

        System.Console.WriteLine( "A と B は同じですか:{0}", resultAB );
        System.Console.WriteLine( "A と C は同じですか:{0}", resultAC );
        System.Console.WriteLine( "B と C は同じですか:{0}", resultBC );

        // 入力待ち用
        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:清次郎],
dataB:[ID:0, Name:正一郎], [ID:5, Name:清次郎],
dataC:[ID:4, Name:征史郎],
A と B は同じですか:False
A と C は同じですか:False
B と C は同じですか:False

 dataAdataBに入っている要素のプロパティは同じ値ですので、dataAdataBの比較はTrueが返ってきそうなものですが、結果はFalseが返ってきています。
 これはプロパティの値は一緒でも、別々でnewを使ってインスタンス化したデータです。
 そのため参照しているものが各々で違います。
 だから、SequenceEqual()Falseを返してしまうのです。

 以下のような書き方の場合には、大丈夫です。
 同じ参照を使用しているからです。

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 valueA    = new Parameter() { ID = 0, Name = "正一郎" };
        Parameter valueB    = new Parameter() { ID = 5, Name = "清次郎" };
        Parameter valueC    = new Parameter() { ID = 3, Name = "誠三郎" };
        Parameter valueD    = new Parameter() { ID = 4, Name = "征史郎" };

        // 名前とIDのデータ。
        Parameter[] dataA = new Parameter[] { valueA, valueB };
        Parameter[] dataB = new Parameter[] { valueA, valueB };
        Parameter[] dataC = new Parameter[] { valueD };

        // 要素は同じかな?
        bool resultAB = dataA.SequenceEqual( dataB );
        bool resultAC = dataA.SequenceEqual( dataC );
        bool resultBC = dataB.SequenceEqual( dataC );

        // 結果発表
        System.Console.WriteLine( "dataA:{0}", dataA.Text() );
        System.Console.WriteLine( "dataB:{0}", dataB.Text() );
        System.Console.WriteLine( "dataC:{0}", dataC.Text() );

        System.Console.WriteLine( "A と B は同じですか:{0}", resultAB );
        System.Console.WriteLine( "A と C は同じですか:{0}", resultAC );
        System.Console.WriteLine( "B と C は同じですか:{0}", resultBC );

        // 入力待ち用
        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:清次郎],
dataB:[ID:0, Name:正一郎], [ID:5, Name:清次郎],
dataC:[ID:4, Name:征史郎],
A と B は同じですか:True
A と C は同じですか:False
B と C は同じですか:False

この要素が同じだと判定する条件を作っちゃう

 しかし、参照の違いではなく、プロパティの値による違いによって成否を返してほしいときもあると思います。
 そういう場合は比較用の処理を作成してみましょう。
 IEqualityComparerを継承したクラスを作成し、SequenceEqual()の第二引数に渡すことで、独自の比較処理を作ることができます。

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

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

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

    // 比較用のクラス。
    // Parameterkクラスに組み込んじゃってもいいけどね。
    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 )
        {
            return i_obj.ID ^ i_obj.Name.GetHashCode();
        }
    }

    static void Main( string[] args )
    {
        // 名前とIDのデータ。
        Parameter[] dataA = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 5, Name = "清次郎" },
        };
        Parameter[] dataB = new Parameter[]
        {
            new Parameter() { ID = 0, Name = "正一郎" },
            new Parameter() { ID = 5, Name = "清次郎" },
        };
        Parameter[] dataC = new Parameter[]
        {
            new Parameter() { ID = 4, Name = "征史郎" },
        };

        // 要素は同じかな?
        ParameterComparer comparer  = new ParameterComparer();
        bool resultAB = dataA.SequenceEqual( dataB, comparer );
        bool resultAC = dataA.SequenceEqual( dataC, comparer );
        bool resultBC = dataB.SequenceEqual( dataC, comparer );

        // 結果発表
        System.Console.WriteLine( "dataA:{0}", dataA.Text() );
        System.Console.WriteLine( "dataB:{0}", dataB.Text() );
        System.Console.WriteLine( "dataC:{0}", dataC.Text() );

        System.Console.WriteLine( "A と B は同じですか:{0}", resultAB );
        System.Console.WriteLine( "A と C は同じですか:{0}", resultAC );
        System.Console.WriteLine( "B と C は同じですか:{0}", resultBC );

        // 入力待ち用
        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:清次郎],
dataB:[ID:0, Name:正一郎], [ID:5, Name:清次郎],
dataC:[ID:4, Name:征史郎],
A と B は同じですか:True
A と C は同じですか:False
B と C は同じですか:False

 自作した比較処理で判定していますので、参照の違いではなく、要素内の値の違いによって判定されるようになりました。

 このあたりを工夫しながら、SequenceEqual()を七兆回使ってみてください。

LINQのリンク