for WPF developers
Home Profile Tips 全記事一覧

Distinct 拡張メソッドで重複する要素をシーケンスから除外する

(2017/03/07 20:51:58 created.)

Distinct 拡張メソッドはシーケンス内で重複する要素を除外することができます。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Linq;
  5.  
  6.     class Program
  7.     {
  8.         static void Main(string[] args)
  9.         {
  10.             var numbers = new int[] { 1, 2, 1, 3, 4, 2, 5 };
  11.             Console.WriteLine("コレクションの要素は {{ {0} }} です。", string.Join(", ", numbers));
  12.  
  13.             var newNumbers = numbers.Distinct();
  14.             Console.WriteLine("コレクションの要素は {{ {0} }} です。", string.Join(", ", newNumbers));
  15.  
  16.             Console.ReadKey();
  17.         }
  18.     }
  19. }


数値型のシーケンスであれば、特に引数を必要とすることなく重複する要素を除外できます。

次に、以下のような Person クラスを定義します。

Person.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.  
  5.     /// <summary>
  6.     /// 人物データを表します。
  7.     /// </summary>
  8.     public class Person
  9.     {
  10.         /// <summary>
  11.         /// 氏名を取得または設定します。
  12.         /// </summary>
  13.         public string Name { get; set; }
  14.  
  15.         /// <summary>
  16.         /// 年齢を取得または設定します。
  17.         /// </summary>
  18.         public int Age { get; set; }
  19.     }
  20. }

このような Person クラスを使った Distinct 拡張メソッドのコード例とその実行結果を次に示します。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var p1 = new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2) };
  12.             var p2 = new Person() { Name = "鈴木 ほのか", Date = new DateTime(2014, 3, 24) };
  13.             var p3 = new Person() { Name = "小池 哲司", Date = new DateTime(2002, 6, 13) };
  14.             var people = new List<Person>() { p1, p2, p1, p3 };
  15.  
  16.             foreach (var p in people)
  17.             {
  18.                 Console.WriteLine(p.Name);
  19.             }
  20.             Console.WriteLine("重複要素を除外します。");
  21.             var newPeople = people.Distinct();
  22.             foreach (var p in newPeople)
  23.             {
  24.                 Console.WriteLine(p.Name);
  25.             }
  26.  
  27.             Console.ReadKey();
  28.         }
  29.     }
  30. }


ところで、インスタンスは異なるけれど同じ名前のものは除外したい、というようなことも考えられます。ところが、次のようなコードでは実現できません。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var p1 = new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2) };
  12.             var p2 = new Person() { Name = "鈴木 ほのか", Date = new DateTime(2014, 3, 24) };
  13.             var p3 = new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2) };
  14.             var people = new List<Person>() { p1, p2, p1, p3 };
  15.  
  16.             foreach (var p in people)
  17.             {
  18.                 Console.WriteLine(p.Name);
  19.             }
  20.             Console.WriteLine("重複要素を除外します。");
  21.             var newPeople = people.Distinct();
  22.             foreach (var p in newPeople)
  23.             {
  24.                 Console.WriteLine(p.Name);
  25.             }
  26.  
  27.             Console.ReadKey();
  28.         }
  29.     }
  30. }


Distinct 拡張メソッドが各要素を "重複" と判断するために、ハッシュコードによる比較と、等値比較演算子による評価を使用しています。特に指定しない場合、参照型ではインスタンス毎にハッシュコードが異なるため、そのプロパティ値が同一であったとしてもこれは "重複" とはみなされません。

カスタムクラスのプロパティ値を比較対象とする場合は、そのカスタムクラスが IEquatable<T> インターフェースを実装し、Equals メソッドと GetHashCode メソッドを適切に実装する必要があります。

それでは Person クラスに IEquatable<Person> インターフェースを実装しましょう。

Person.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.  
  6.     /// <summary>
  7.     /// 人物データを表します。
  8.     /// </summary>
  9.     public class Person : IEquatable<Person>
  10.     {
  11.         /// <summary>
  12.         /// 氏名を取得または設定します。
  13.         /// </summary>
  14.         public string Name { get; set; }
  15.  
  16.         /// <summary>
  17.         /// 更新日付を取得または設定します。
  18.         /// </summary>
  19.         public DateTime Date { get; set; }
  20.  
  21.         /// <summary>
  22.         /// Name プロパティによる等値比較演算子を定義します。
  23.         /// </summary>
  24.         /// <param name="other">比較対象とするオブジェクトを指定します。</param>
  25.         /// <returns>Name プロパティが一致した場合に true を返します。</returns>
  26.         public bool Equals(Person other)
  27.         {
  28.             return this.Name == other.Name;
  29.         }
  30.  
  31.         /// <summary>
  32.         /// ハッシュコードを取得します。
  33.         /// </summary>
  34.         /// <returns>Name プロパティによるハッシュコード値を返します。</returns>
  35.         public override int GetHashCode()
  36.         {
  37.             return this.Name.GetHashCode();
  38.         }
  39.     }
  40. }

ところで、IEquatable<T> インターフェースのメンバは Equals() メソッドだけですが、ここでは GetHashCode() メソッドをオーバーライドしています。これは、Distinct 拡張メソッドが等値比較演算子以外にもハッシュコードによる比較もおこなっているからです。

Distinct 拡張メソッドの内部実装を逆コンパイルして確認してみると、次のようなコードが見つかります。

ILSpy による逆コンパイル結果
  1. if (this.slots[i].hashCode == num && this.comparer.Equals(this.slots[i].value, value))
  2. {
  3.     return true;
  4. }

this がなんなのかはともかく、ハッシュコードと Equals() メソッドによって一致しているかどうかを判定していることがわかります。Equls() メソッドは IEquatable<T> インターフェースのメンバですが、ハッシュコード値を取得するための GetHashCode() メソッドは object 型の仮想メソッドです。デフォルトではインスタンス毎に異なるハッシュ値が返ってきてしまうため、今回のように Name プロパティの一致によって "重複" していることを判断する場合、GetHashCode() メソッドを別途オーバーライドし、Name プロパティの値が同じときに同じハッシュコード値を返すようにする必要があるということです。

以上から、IEquatable<T> を実装した Person クラスを使用することで、先ほどの実行結果は次のように変わります。


当然ですが、同一インスタンスのものも Name プロパティのハッシュコード値が一致し、Equals() メソッドによる評価も true となったため、除外されています。

Distinct 拡張メソッドには、等値比較演算子を外部指定できるオーバーロードも用意されています。例えばIEquatable<T> インターフェースを実装していない Person クラスが既に用意されていて、こちらの実装を変更できない場合、外部に IEqualityComparer<T> インターフェースを実装したクラスを用意することで同じことが実現できます。

比較用に使用する IEqualityComparer<Person> インターフェースを実装したクラスを次のように定義します。

PersonComparer.cs
  1. namespace Tips_Linq
  2. {
  3.     using System.Collections.Generic;
  4.  
  5.     /// <summary>
  6.     /// Person クラスに対する等値比較子を表します。
  7.     /// </summary>
  8.     public class PersonComparer : IEqualityComparer<Person>
  9.     {
  10.         public static readonly PersonComparer NameComparer = new PersonComparer();
  11.  
  12.         /// <summary>
  13.         /// 指定された Person クラスのオブジェクトが等しいかどうかを確認します。
  14.         /// </summary>
  15.         /// <param name="x">比較基準を指定します。</param>
  16.         /// <param name="y">比較対象を指定します。</param>
  17.         /// <returns>Name プロパティが等しい場合に true を返します。</returns>
  18.         public bool Equals(Person x, Person y)
  19.         {
  20.             return x.Name == y.Name;
  21.         }
  22.  
  23.         /// <summary>
  24.         /// ハッシュ値を取得します。
  25.         /// </summary>
  26.         /// <param name="obj">ハッシュ値を算出するオブジェクトを指定します。</param>
  27.         /// <returns>算出したハッシュ値を返します。</returns>
  28.         public int GetHashCode(Person obj)
  29.         {
  30.             return obj.Name.GetHashCode();
  31.         }
  32.     }
  33. }

IEqualityComparer<T> インターフェースのメンバは Equals() メソッドと GetHashCode() メソッドです。各メソッドの実装については、Person クラスに IEquatable インターフェースを実装したときと同様に Name プロパティによる処理とします。

Program.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var p1 = new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2) };
  12.             var p2 = new Person() { Name = "鈴木 ほのか", Date = new DateTime(2014, 3, 24) };
  13.             var p3 = new Person() { Name = "田中 淳平", Date = new DateTime(2011, 5, 2) };
  14.             var people = new List<Person>() { p1, p2, p1, p3 };
  15.  
  16.             foreach (var p in people)
  17.             {
  18.                 Console.WriteLine(p.Name);
  19.             }
  20.             Console.WriteLine("重複要素を除外します。");
  21.             var newPeople = people.Distinct(PersonComparer.NameComparer);
  22.             foreach (var p in newPeople)
  23.             {
  24.                 Console.WriteLine(p.Name);
  25.             }
  26.  
  27.             Console.ReadKey();
  28.         }
  29.     }
  30. }

自分で実装を変更できるクラスならそのクラスに IEquatable<T> インターフェースを実装し、そうでない場合は外部に IEqualityComparer<T> インターフェースを実装したクラスを用意することで対応しましょう。