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

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

【C#,LINQ】Contains~配列やリストの中で指定した要素があるかを判定したいとき~

 C#のLINQの関数であるContains()の使い方についてです。
 配列やリストなどのシーケンス内にて、指定した要素があるかを判定することが出来ます。
f:id:urahimono:20180625230920p:plain


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

この要素をもっていますか?

 配列の中に、この要素はあるかどうかがわからない……。
 そんなときはLINQContains()です。

 引数に探したい要素を渡すことで、配列やリストの中にその要素を含んでいるかが調べられます。

public static bool Contains<TSource>( this IEnumerable<TSource> source, TSource value );
Enumerable.Contains(TSource) メソッド (IEnumerable(TSource), TSource) (System.Linq)

 ただ、この要素が値型なのか参照型なのかで少し挙動が変わるので気をつけねばなりませんよ。

値型の場合

 ではまず値型からいってみましょう。
 特にintなどの数値型の場合は特に何も考えずに使ってもあんまり問題は発生しないかもしれません。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9の数値データ
        int[]   numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        bool reaultA = numbers.Contains( 8 );
        bool reaultB = numbers.Contains( 3 );
        bool reaultC = numbers.Contains( 12 );

        // 結果発表
        System.Console.WriteLine( "numbers:{0}", numbers.Text() );
        System.Console.WriteLine( " 8の要素はありますか:{0}", reaultA );
        System.Console.WriteLine( " 3の要素はありますか:{0}", reaultB );
        System.Console.WriteLine( "12の要素はありますか:{0}", reaultC );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

numbers:[0], [1], [2], [3], [4], [5], [6], [7], [8], [9],
8の要素はありますか:True
3の要素はありますか:True
12の要素はありますか: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のデータ
        List<Parameter> parameters  = new List<Parameter>();

        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 = 9, Name = "征史郎" };

        parameters.Add( valueA );
        parameters.Add( valueB );
        parameters.Add( valueC );

        bool reaultA = parameters.Contains( valueA );
        bool reaultB = parameters.Contains( valueC );
        bool reaultC = parameters.Contains( valueD );

        // 結果発表
        System.Console.WriteLine( "parameters:{0}", parameters.Text() );
        System.Console.WriteLine( "正一郎の要素はありますか:{0}", reaultA );
        System.Console.WriteLine( "誠三郎の要素はありますか:{0}", reaultB );
        System.Console.WriteLine( "征史郎の要素はありますか:{0}", reaultC );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

parameters:[ID:0, Name:正一郎], [ID:5, Name:清次郎], [ID:3, Name:誠三郎],
正一郎の要素はありますか:True
誠三郎の要素はありますか:True
征史郎の要素はありますか:False

 うまく動いていますね。
 
 では今度は少しやり方を変えてみましょう。
 以下のコードのvalueAvalueBは中身のプロパティの値は同じですが、新しくnewを使って作成されています。
 そしてリストの中に入っているのは、valueAのみです。
 どういう結果になるのでしょうか。

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のデータ
        List<Parameter> parameters  = new List<Parameter>();

        // 参照が違うだけで、設定されているプロパティ値は同じ!
        Parameter valueA    = new Parameter() { ID = 0, Name = "征史郎" };
        Parameter valueB    = new Parameter() { ID = 0, Name = "征史郎" };
        parameters.Add( valueA );

        bool reaultA = parameters.Contains( valueA );
        bool reaultB = parameters.Contains( valueB );

        // 結果発表
        System.Console.WriteLine( "parameters:{0}", parameters.Text() );
        System.Console.WriteLine( "同じ参照を使った場合:{0}", reaultA );
        System.Console.WriteLine( "違う参照を使った場合:{0}", reaultB );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

parameters:[ID:0, Name:征史郎],
同じ参照を使った場合:True
違う参照を使った場合:False

 このような結果になります。
 中身のプロパティの値は同じなのですが、valueAvalueBもどちらもnewを使ってインスタンスを作っているので、参照先は違います。
 そのためContains()は、違う要素だと判定してしまうのです。

 参照のリストや配列に対してContains()を使う場合はこの点に気をつけなくてはいけません。

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

 さて、先ほどの例の場合は、参照が違うのならFalseを返してくれました。
 ですが、参照型の場合でも、中身のプロパティが同じか否かで要素の有無を確認したい場合にはどうすればいいのでしょうか。

 Contains()にはオーバーロードされた関数が用意されています。
 第二引数に比較用のデータを渡すことで、比較処理を独自に指定することが出来ます。
 比較用のデータはIEqualityComparerを継承したものでなくてはいけません。

public static bool Contains<TSource>( this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer );
Enumerable.Contains(TSource) メソッド (IEnumerable(TSource), 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のデータ
        List<Parameter> parameters  = new List<Parameter>();

        // 参照が違うだけで、設定されているプロパティ値は同じ!
        Parameter valueA    = new Parameter() { ID = 0, Name = "征史郎" };
        Parameter valueB    = new Parameter() { ID = 0, Name = "征史郎" };
        parameters.Add( valueA );

        ParameterComparer comparer  = new ParameterComparer();
        bool reaultA = parameters.Contains( valueA, comparer );
        bool reaultB = parameters.Contains( valueB, comparer );

        // 結果発表
        System.Console.WriteLine( "parameters:{0}", parameters.Text() );
        System.Console.WriteLine( "同じ参照を使った場合:{0}", reaultA );
        System.Console.WriteLine( "違う参照を使った場合:{0}", reaultB );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

parameters:[ID:0, Name:征史郎],
同じ参照を使った場合:True
違う参照を使った場合:True

 今回の結果はどちらもTrueが返るようになりました。
 参照型に限らず、比較用の情報を渡すことで、独自の比較が行えるようになります。

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

LINQのリンク