for WPF developers
Home Profile Tips 全記事一覧

Contains 拡張メソッドで指定した要素が含まれているかどうかを確認する

(2017/03/07 20:33:05 created.)

(2017/03/13 8:09:41 modified.)

Contains 拡張メソッドを使うと、指定した値がシーケンス内に含まれているかどうかを確認できます。

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, 3 };
  11.             Console.WriteLine("コレクションの要素は {{ {0} }} です。", string.Join(", ", numbers));
  12.  
  13.             var value = 2;
  14.             var isContains = numbers.Contains(value);
  15.             Console.WriteLine(value.ToString() + " はシーケンス内に" + (isContains ? "あります。" : "ありません。"));
  16.  
  17.             Console.ReadKey();
  18.         }
  19.     }
  20. }


それでは次のような Person クラスを定義し、このクラスのコレクションに対して同じことをしてみましょう。

Person.cs
  1. namespace Tips_Linq
  2. {
  3.     using System;
  4.  
  5.     /// 
  6.     /// 人物データを表します。
  7.     /// 
  8.     public class Person
  9.     {
  10.         /// 
  11.         /// 氏名を取得または設定します。
  12.         /// 
  13.         public string Name { get; set; }
  14.  
  15.         /// 
  16.         /// 年齢を取得または設定します。
  17.         /// 
  18.         public int Age { get; set; }
  19.     }
  20. }
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 people = GetPeople();
  12.             foreach (var p in people)
  13.             {
  14.                 Console.WriteLine(p.Name);
  15.             }
  16.  
  17.             var person = new Person() { Name = "鈴木 ほのか", Age = 26 };
  18.             var isContains = people.Contains(person);
  19.             Console.WriteLine(person.Name + " はシーケンス内に" + (isContains ? "います。" : "いません。"));
  20.  
  21.             Console.ReadKey();
  22.         }
  23.  
  24.         /// 
  25.         /// 人物コレクションの列挙子を取得します。
  26.         /// 
  27.         /// 
  28.         static IEnumerable<Person> GetPeople()
  29.         {
  30.             yield return new Person() { Name = "田中 淳平", Age = 37 };
  31.             yield return new Person() { Name = "鈴木 ほのか", Age = 26 };
  32.             yield return new Person() { Name = "小池 哲司", Age = 22 };
  33.             yield return new Person() { Name = "恩田 進", Age = 42 };
  34.             yield return new Person() { Name = "中津山 亜希子", Age = 20 };
  35.         }
  36.     }
  37. }


上記のコードでは「鈴木ほのか」という人物がシーケンス内に存在しないかのような結果になっています。

Contains 拡張メソッドでは、特に指定しない場合は既定の等値比較子を使って値を比較しています。そのため、参照型を比較する場合、そのインスタンス自身が等しいかどうかを比較されるようになります。上記の例では、「鈴木ほのか」という同じ名前であってもまったく別のインスタンスを 17 行目で生成し、これとシーケンス内の要素を比較しているため、インスタンスが異なる、という意味でシーケンス内には含まれず、結果は false となっています。

それでは、Person クラスのコレクションに対して「鈴木ほのか」という名前の人がいるかどうかを確認するときはどうすればいいでしょうか。Select 拡張メソッドを併用する方法もありますが、ここではあえて Contains 拡張メソッドのみで対応できるようにしてみましょう。

Contains 拡張メソッドには次のようなオーバーロードが用意されています。

ILSpy の逆コンパイル結果
  1. public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer)
  2. {
  3.     if (comparer == null)
  4.     {
  5.         comparer = EqualityComparer<TSource>.Default;
  6.     }
  7.     if (source == null)
  8.     {
  9.         throw Error.ArgumentNull("source");
  10.     }
  11.     foreach (TSource current in source)
  12.     {
  13.         if (comparer.Equals(current, value))
  14.         {
  15.             return true;
  16.         }
  17.     }
  18.     return false;
  19. }

14 行目で、第 2 引数に指定される IEqualityComparer<T> インターフェースに実装される Equals メソッドを使って等値かどうかを判定しています。Person クラスを比較するための IEqualityComparer<Person> インターフェースを実装した比較用クラスを実装すれば、その比較子にしたがった等値比較をおこなうことができるようになるということになります。

それでは実際に IEqualityComparer<Person> を実装した PersonComarer クラスを作りましょう。

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

使いやすくするために 10 行目にあえて静的メンバとしてインスタンスを保持しています。このクラスを Contains 拡張メソッドに指定することで、先ほどとは違った結果が得られます。

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 people = GetPeople();
  12.             foreach (var p in people)
  13.             {
  14.                 Console.WriteLine(p.Name);
  15.             }
  16.  
  17.             var person = new Person() { Name = "鈴木 ほのか", Age = 26 };
  18.             var isContains = people.Contains(person, PersonComparer.NameComparer);
  19.             Console.WriteLine(person.Name + " はシーケンス内に" + (isContains ? "います。" : "いません。"));
  20.  
  21.             Console.ReadKey();
  22.         }
  23.  
  24.         /// 
  25.         /// 人物コレクションの列挙子を取得します。
  26.         /// 
  27.         /// 
  28.         static IEnumerable<Person> GetPeople()
  29.         {
  30.             yield return new Person() { Name = "田中 淳平", Age = 37 };
  31.             yield return new Person() { Name = "鈴木 ほのか", Age = 26 };
  32.             yield return new Person() { Name = "小池 哲司", Age = 22 };
  33.             yield return new Person() { Name = "恩田 進", Age = 42 };
  34.             yield return new Person() { Name = "中津山 亜希子", Age = 20 };
  35.         }
  36.     }
  37. }


このように、参照型の場合は等値比較子を指定しないと意図しない結果となることがあるため注意が必要です。数値型の場合は数値そのものを評価するため、同じインスタンスかどうかを区別することが目的でない限りは問題ありません。また、string 型は参照型でありながら、その既定の等値比較子は数値型と同じように振る舞います。コード例と実行結果を次に示します。これは string 型が Immutable であることが起因していますが詳細は割愛します。

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 names = new string[] { "田中 淳平", "鈴木 ほのか", "小池 哲司", "恩田 進", "中津山 亜希子" };
  11.  
  12.             var name = "鈴木 ほのか";
  13.             var isContains = names.Contains(name);
  14.             Console.WriteLine(name + " はシーケンス内に" + (isContains ? "います。" : "いません。"));
  15.  
  16.             Console.ReadKey();
  17.         }
  18.     }
  19. }