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

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

【C#,LINQ】Distinct~配列やリスト内の重複した要素を削除したいとき~

 C#のLINQの関数であるDistinct()の使い方についてです。
 配列やリストの中で重複した要素を削除して、重複のなくすことが出来ます。
f:id:urahimono:20180702001927p:plain


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

重複した要素を削除して一つだけに

 破裂やリストの中にまったく同じ要素があるときありますよね。
 そんな重複した要素のみを削除して、同じ要素がないようにするのは、地味に面倒くさかったりします。

 そんなときはLINQDistinct()を使うことで、簡単に重複要素を削除することが出来ます。

public static IEnumerable<TSource> Distinct<TSource>( this IEnumerable<TSource> source );
Enumerable.Distinct(TSource) メソッド (IEnumerable(TSource)) (System.Linq)

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, 3, 3, 2 };
        List<float> dataB   = new List<float>() { 1.5f, 1.5f, 1.5f, 1.5f };
        string[]    dataC   = new string[]      { "征史郎", "征四郎", "征史郎", "正史郎" };

        // 重複は許さないぞ!
        IEnumerable<int>    dataA_D = dataA.Distinct();
        IEnumerable<float>  dataB_D = dataB.Distinct();
        IEnumerable<string> dataC_D = dataC.Distinct();

        System.Console.WriteLine( "dataA         :{0}", dataA.Text() );
        System.Console.WriteLine( "dataA Distinct:{0}", dataA_D.Text() );
        System.Console.WriteLine( "dataB         :{0}", dataB.Text() );
        System.Console.WriteLine( "dataB Distinct:{0}", dataB_D.Text() );
        System.Console.WriteLine( "dataC         :{0}", dataC.Text() );
        System.Console.WriteLine( "dataC Distinct:{0}", dataC_D.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 :[0], [1], [3], [3], [2],
dataA Distinct:[0], [1], [3], [2],
dataB :[1.5], [1.5], [1.5], [1.5],
dataB Distinct:[1.5],
dataC :[征史郎], [征四郎], [征史郎], [正史郎],
dataC Distinct:[征史郎], [征四郎], [正史郎],

比較条件を指定したいとき

 さて、先ほどのような値型の要素に関してはDistinct()を呼ぶだけでいいんですが、参照型の場合はどうでしょう。
 以下の例をご覧ください。

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}", Name, Age );
        }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Age = 52 },
            new Parameter() { Name = "征史郎", Age = 18 },
            new Parameter() { Name = "征史郎", Age = 18 },
        };

        // 重複は許さないぞ!
        IEnumerable<Parameter> parameters_D = parameters.Distinct();

        System.Console.WriteLine( "parameters         :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters Distinct:{0}", parameters_D.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

parameters :[Name:正一郎, Age:52], [Name:征史郎, Age:18], [Name:征史郎,
Age:18],
parameters Distinct:[Name:正一郎, Age:52], [Name:征史郎, Age:18], [Name:征史郎,
Age:18],

 同じクラスのプロパティの値を持つ要素が複数あるのに、Distinct()によって削除されていません。
 これは参照しているものが異なるため、別の要素扱いになっているからです。
 今回のコードでは全ての要素が別々にnewを使ってクラスを生成しています。
 そのため、設定するプロパティの値が同じでも違う要素になるのです。

 以下の書き方の場合は、二番目と三番目の要素は同じ参照を使っているのでDistinct()が有効になります。

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}", Name, Age );
        }
    }

    static void Main( string[] args )
    {
        Parameter paramA = new Parameter() { Name = "正一郎", Age = 52 };
        Parameter paramB = new Parameter() { Name = "征史郎", Age = 18 };
        Parameter[] parameters = new Parameter[]
        {
            paramA,
            paramB,
            paramB,
        };

        // 重複は許さないぞ!
        IEnumerable<Parameter> parameters_D = parameters.Distinct();

        System.Console.WriteLine( "parameters         :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters Distinct:{0}", parameters_D.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

parameters :[Name:正一郎, Age:52], [Name:征史郎, Age:18], [Name:征史郎,
Age:18],
parameters Distinct:[Name:正一郎, Age:52], [Name:征史郎, Age:18],

 参照型の場合の挙動はわかりました。
 とはいえ参照型の場合でも、参照している要素の違いではなく、要素の中身のプロパティが同じかどうかで判断してほしい場合もあると思います。
 そういった場合は、比較処理を自作することができます。

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

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

 以下のようなIEqualityComparerを継承した比較用のクラスを作ることで、同じかどうかの判定を自作できます。

Program.cs

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

    public int GetHashCode( Parameter i_obj )
    {
        return i_obj.Age ^ i_obj.Name.GetHashCode();
    }
}

 この比較用のクラスをDistinct()に渡してあげれば、参照型であっても中身のプロパティで違いを判定できます。

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int      Age     { get; set; }
        
        public override string ToString()
        {
            return string.Format( "Name:{0}, Age:{1}", Name, Age );
        }
    }

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

        public int GetHashCode( Parameter i_obj )
        {
            return i_obj.Age ^ i_obj.Name.GetHashCode();
        }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Age = 52 },
            new Parameter() { Name = "征史郎", Age = 18 },
            new Parameter() { Name = "征史郎", Age = 18 },
        };

        // 重複は許さないぞ!
        ParameterComparer Comparer = new ParameterComparer();
        IEnumerable<Parameter> parameters_D = parameters.Distinct( Comparer );

        System.Console.WriteLine( "parameters         :{0}", parameters.Text() );
        System.Console.WriteLine( "parameters Distinct:{0}", parameters_D.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

parameters :[Name:正一郎, Age:52], [Name:征史郎, Age:18], [Name:征史郎,
Age:18],
parameters Distinct:[Name:正一郎, Age:52], [Name:征史郎, Age:18],

 このように参照が違っても、要素の中身のプロパティが同じだと判定すればDistinct()が有効になります。

 Distinct()を七兆回ほど使って、世の中の無駄を削除してみてください。

LINQのリンク