C#を利用してアプリケーションを開発する際、特定のデータが特定の集合の中に存在するかどうかを確認する操作は非常に一般的です。

文字列の中に特定の単語が含まれているか、あるいはリストや配列の中に特定のオブジェクトが格納されているかを判定するために、C#では Containsメソッド が用意されています。

このメソッドは、直感的な構文でありながら、データ型や検索条件によって内部的な挙動やパフォーマンスが異なるため、正しく理解して使い分けることが重要です。

Containsメソッドの基本概念

Containsメソッドは、対象となるシーケンス(文字列、リスト、配列など)に特定の要素が含まれている場合に true を、含まれていない場合に false を返すメソッドです。

C#においてContainsメソッドは、主に以下の3つのコンテキストで使用されます。

  1. String.Contains:文字列内に部分一致する文字列があるかを判定
  2. List<T>.Contains:動的配列の中に指定された要素があるかを判定
  3. Enumerable.Contains:配列やIEnumerableを実装するコレクションに対して、LINQを利用して判定

これらのメソッドは名前こそ同じですが、定義されている場所や引数の取り方が異なります。

特に文字列の検索においては、大文字小文字を区別するかどうか、あるいは文化圏(カルチャ)に依存した比較を行うかどうかといった詳細な設定が可能です。

文字列検索におけるContainsメソッドの使い方

String.Contains は、ある文字列の中に特定の文字列が含まれているかを調べます。

古くからのC#(.NET Framework)では「引数に文字列を1つ取る」形式が一般的でしたが、現代のC#(.NET Core 2.1以降や.NET 5/6/7/8以降)では、比較オプションを指定できるオーバーロードが追加されています。

基本的な文字列検索

まずは、最もシンプルな使い方を見てみましょう。

C#
using System;

class Program
{
    static void Main()
    {
        string message = "C#プログラミングの世界へようこそ";
        string keyword = "プログラミング";

        // 基本的なContainsの使用
        bool result = message.Contains(keyword);

        Console.WriteLine($"キーワード '{keyword}' が含まれているか: {result}");
    }
}
実行結果
キーワード 'プログラミング' が含まれているか: True

このメソッドは、大文字と小文字を厳密に区別する という点に注意が必要です。

例えば、ターゲットが “C#” で検索キーワードが “c#” の場合、結果は false となります。

大文字・小文字を区別しない検索

かつてのC#では、大文字小文字を無視するために一度 ToLower()ToUpper() を呼び出して文字列を統一してから比較する方法が一般的でした。

しかし、この方法は 新しい文字列インスタンスを生成するため、メモリ効率(GCへの負荷)の観点から推奨されません。

最新の.NETでは、StringComparison 列挙型を引数に渡すことで、効率的に検索を行うことができます。

C#
using System;

class Program
{
    static void Main()
    {
        string text = "Welcome to .NET Development";
        string search = "development";

        // StringComparison.OrdinalIgnoreCase を使用して大文字小文字を無視
        bool result = text.Contains(search, StringComparison.OrdinalIgnoreCase);

        Console.WriteLine($"大文字小文字を無視した検索結果: {result}");
    }
}
実行結果
大文字小文字を無視した検索結果: True
指定する値内容
StringComparison.Ordinalバイナリレベルで比較(デフォルト・高速)
StringComparison.OrdinalIgnoreCaseバイナリレベルで大文字小文字を無視して比較
StringComparison.CurrentCulture現在のスレッドのカルチャ設定を使用して比較
StringComparison.CurrentCultureIgnoreCase現在のカルチャ設定で大文字小文字を無視して比較

一般的には、StringComparison.OrdinalIgnoreCase を使用するのが最もパフォーマンスが良く、意図した動作になりやすいため推奨されます。

List<T> におけるContainsメソッドの使い方

List<T>.Contains は、リスト内に特定の要素が存在するかをチェックします。

このメソッドの挙動は、リストに格納されているデータの型(値型か参照型か)によって異なります。

値型(int, double等)の検索

数値などの値型の場合、単純な値の比較が行われます。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };

        bool hasThirty = numbers.Contains(30);
        bool hasSixty = numbers.Contains(60);

        Console.WriteLine($"30が含まれるか: {hasThirty}");
        Console.WriteLine($"60が含まれるか: {hasSixty}");
    }
}
実行結果
30が含まれるか: True
60が含まれるか: False

参照型(クラス・オブジェクト)の検索

クラスなどの参照型を検索する場合、デフォルトでは 「参照(メモリ上のアドレス)が一致しているか」 で判定されます。

つまり、プロパティの値がすべて同じであっても、インスタンスが別であれば false が返されます。

C#
using System;
using System.Collections.Generic;

class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        var user1 = new User { Id = 1, Name = "Tanaka" };
        var list = new List<User> { user1 };

        // 同じプロパティを持つが別インスタンス
        var searchUser = new User { Id = 1, Name = "Tanaka" };

        Console.WriteLine($"同一インスタンスの検索: {list.Contains(user1)}");
        Console.WriteLine($"別インスタンスの検索: {list.Contains(searchUser)}");
    }
}
実行結果
同一インスタンスの検索: True
別インスタンス of 'Tanaka' の検索: False

もし、プロパティの値に基づいて「同一である」とみなしたい場合は、Equals メソッドのオーバーライドや、IEquatable<T> インターフェースの実装が必要です。

配列におけるContainsメソッドの使い方

配列(Array)自体には、厳密にはインスタンスメソッドとしての Contains は存在しません。

しかし、System.Linq 名前空間をインポートすることで、拡張メソッドとして Contains を利用できるようになります。

C#
using System;
using System.Linq; // これが必要

class Program
{
    static void Main()
    {
        string[] fruits = { "Apple", "Banana", "Cherry" };

        // LINQのEnumerable.Containsを使用
        bool hasBanana = fruits.Contains("Banana");

        Console.WriteLine($"Bananaが含まれるか: {hasBanana}");
    }
}
実行結果
Bananaが含まれるか: True

配列に対して Contains を使用する場合、内部的にはループ処理が行われています。

要素数が非常に多い場合、LINQの拡張メソッドはオーバーヘッドが生じる可能性があるため、パフォーマンスがクリティカルな場面では Array.Exists や後述する HashSet を検討してください。

独自のオブジェクトでContainsを正しく動作させる方法

先述の通り、カスタムクラスのリストで Contains を使用する場合、デフォルトの挙動(参照一致)では不十分なケースが多いです。

「IDが一致していれば同じデータとみなす」といったロジックを実装するには、IEquatable<T> インターフェースを実装するのが最もクリーンな方法です。

C#
using System;
using System.Collections.Generic;

class Product : IEquatable<Product>
{
    public string Code { get; set; }
    public string Name { get; set; }

    // IEquatableの実装
    public bool Equals(Product other)
    {
        if (other == null) return false;
        return this.Code == other.Code; // コードが同じなら同一品とする
    }

    // Equalsをオーバーライドする場合はGetHashCodeもセットで実装するのが原則
    public override bool Equals(object obj) => Equals(obj as Product);
    public override int GetHashCode() => Code?.GetHashCode() ?? 0;
}

class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Code = "A100", Name = "PC" },
            new Product { Code = "B200", Name = "Monitor" }
        };

        var target = new Product { Code = "A100", Name = "New PC" };

        // IEquatableの実装により、Codeが一致するためTrueとなる
        Console.WriteLine($"商品が含まれているか: {products.Contains(target)}");
    }
}
実行結果
商品が含まれているか: True

このように実装しておくことで、Contains メソッド内部で自動的に Equals(Product other) が呼び出されるようになります。

Containsのパフォーマンスと注意点

Containsメソッドを使用する際には、その計算量(パフォーマンス特性)を理解しておくことが重要です。

計算量 O(n) の壁

List<T>.Contains や配列に対する Contains は、要素の先頭から順番に1つずつチェックを行う 線形探索(Linear Search) です。

要素数を $n$ とすると、計算量は O(n) となります。

つまり、要素数が100万個あるリストに対して Contains を実行すると、最悪の場合100万回の比較が行われます。

大規模データの検索には HashSet<T> を検討

頻繁に検索を行う、かつ要素数が多い場合には、HashSet<T> の使用が適しています。

HashSet<T> は内部的にハッシュテーブルを使用しているため、平均的な計算量は O(1) (定数時間)です。

C#
// Listの場合(遅い可能性がある)
List<string> largeList = GetLargeData();
if (largeList.Contains("SearchItem")) { ... }

// HashSetの場合(非常に高速)
HashSet<string> largeSet = new HashSet<string>(largeList);
if (largeSet.Contains("SearchItem")) { ... }

null の取り扱い

Contains メソッド自体は多くの場合 null を引数として受け取ることができます(文字列の場合は例外が発生するケースもあります)。

  • string.Contains(null):.NET のバージョンによりますが、通常は ArgumentNullException がスローされます。
  • List.Contains(null):リスト内に null が含まれていれば true を返し、含まれていなければ false を返します。例外は発生しません。

検索対象の変数が null になる可能性がある場合は、事前に null チェックを行うか、条件演算子を活用しましょう。

Containsの代替手段と使い分け

状況によっては、Contains 以外のメソッドを使用する方が適切な場合があります。

1. IndexOf メソッド

「含まれているかどうか」だけでなく、「どの位置にあるか」を知りたい場合は IndexOf を使用します。

見つからない場合は -1 が返されます。

2. Any メソッド(LINQ)

「特定の条件を満たす要素があるか」を判定したい場合は、LINQの Any メソッドが柔軟です。

C#
// 価格が1000円以上の商品があるかどうか
bool hasExpensiveProduct = products.Any(p => p.Price >= 1000);

単なる「値が一致するか」のチェックであれば Contains の方がコードが短く読みやすいため、用途に合わせて選択してください。

3. Exists メソッド(List<T> 専用)

List<T>.ExistsAny と同様に述語(ラムダ式)を引数に取ります。

LINQを使用せずにリスト固有の機能で判定したい場合に利用されます。

まとめ

C#の Containsメソッド は、データ検索の基本となる非常に便利なツールです。

今回の内容を整理すると、以下のポイントが重要となります。

  • 文字列検索 では、StringComparison.OrdinalIgnoreCase を指定することで効率的に大文字小文字を無視した検索が可能。
  • リストや配列 では、要素数が増えるとパフォーマンスが低下するため、必要に応じて HashSet<T> を検討する。
  • カスタムオブジェクト を検索する場合は、IEquatable<T> を実装して「何をもって一致とするか」を定義する必要がある。
  • LINQのAny と使い分けることで、より複雑な検索条件にも対応できる。

Containsメソッドを正しく使い分けることは、読みやすく保守性の高い、そしてパフォーマンスに優れたプログラムを書くための第一歩です。

日々の開発において、扱うデータの型や量に応じた最適な選択を心がけましょう。