連載
» 2017年03月15日 05時00分 UPDATE

.NET TIPS:Listの要素を検索するには?[C#/VB]

List<T>クラスのメソッドあるいはLINQ拡張メソッドを利用して、条件に合致する要素をリストから検索する方法を紹介する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 ジェネリックコレクションで最も頻繁に使われるのはList<T>(C#)/List(Of T)(VB)クラス(System.Collections.Generic名前空間)であろう(以降、型引数はC#での表記だけとさせていただく)。本稿では、そのコレクションに含まれる要素を検索する方法を解説する。

 なお、List<T>クラスは.NET Framework 2.0で導入されたものだが、本稿はそれ以降の内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015(またはそれ以降)が必要である。

List<T>コレクションの要素を検索するには?

 通常はLINQ拡張を使えばよい。ただし、要素数が多く、かつ、要素が昇順に並んでいるときに特定の要素を検索する場合は、List<T>クラスのBinarySearchメソッドが高速だ。

 List<T>クラスは.NET Framework 2.0で導入された。LINQは.NET Framework 3.5からである。そのため、List<T>クラスは検索のためのメソッドを独自に持っているのだが、ほとんどは後のLINQ拡張でも同じことが可能なのである。従って、統一的にコードを書くという観点からは、LINQ拡張を使って検索するのがよいだろう。ただし、LINQ拡張ではできないこともある。代表的なものはBinarySearchメソッドである(最後に解説する)。

検索して見つかった要素を新しいコレクションに取り出す例

 実際にList<T>コレクションで検索する例を確認してみよう。

 まずは、条件を満たす要素の全てを新しいコレクションに抽出する例だ。これには、LINQのWhere拡張メソッドを使う。List<T>クラスのFindAllメソッドを使ってもよい。次のコードに、コンソールアプリの例を示す。

using System.Collections.Generic;
using System.Linq;
using static System.Console;

class Program
{
  // コレクションの全要素を出力するメソッド
  static void DisplayItems<T>(IEnumerable<T> collection)
    => WriteLine($"{string.Join(", ", collection)}");

  static void Main(string[] args)
  {
    List<int> list = new List<int> {1,2,3,4,5,6,7,8,9,10,};

    // 偶数を検索して、見つかったものだけのコレクションを作る
    // 【1】LINQのWhere拡張メソッド
    IEnumerable<int> result1 = list.Where(n => n % 2 == 0);
    DisplayItems(result1);
    // 出力:2, 4, 6, 8, 10

    // 【2】List<T>クラスのFindAllメソッド
    List<int> result2 = list.FindAll(n => n % 2 == 0);
    DisplayItems(result2);
    // 出力:2, 4, 6, 8, 10

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

Module Module1
  ' コレクションの全要素を出力するメソッド
  Sub DisplayItems(Of T)(collection As IEnumerable(Of T))
    WriteLine($"{String.Join(", ", collection)}")
  End Sub

  Sub Main()
    Dim list As List(Of Integer) _
      = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    ' 偶数を検索して、見つかったものだけのコレクションを作る
    ' 【1】LINQのWhere拡張メソッド
    Dim result1 As IEnumerable(Of Integer) _
      = list.Where(Function(n) n Mod 2 = 0)
    DisplayItems(result1)
    ' 出力:2, 4, 6, 8, 10

    ' 【2】List<T>クラスのFindAllメソッド
    Dim result2 As List(Of Integer) = list.FindAll(Function(n) n Mod 2 = 0)
    DisplayItems(result2)
    ' 出力:2, 4, 6, 8, 10

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

検索して見つかった要素を新しいコレクションにする例(上:C#、下:VB)
list変数の初期化方法は、「.NET TIPS:構文:コレクションのインスタンス化と同時に要素を追加するには?[C#/VB]」をご覧いただきたい。
C#コードの冒頭から3行目にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
C#のこのDisplayItemsメソッドの書き方については、「.NET TIPS:構文:メソッドやプロパティをラムダ式で簡潔に実装するには?[C# 6.0]」をご覧いただきたい。
DisplayItemsメソッドに出てくる先頭に「$」記号が付いた文字列については、「.NET TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]」の後半(「C# 6.0/VB 14で追加された補間文字列機能を使用する」)を見てほしい。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。

 上のコードで注意したいのは、検索結果の型が違うことだ。LINQのWhere拡張メソッドは遅延実行され、IEnumerable<T>型を返す。List<T>クラスのFindAllメソッドは即時実行され、List<T>型を返す。即時実行(List<T>クラス)の方が多くのメモリを消費する。また、検索結果の全部を使わなかった場合には(例えば、後続のforeach(C#)/For Each(VB)ループ内で処理を途中で打ち切る場合など)、遅延実行(LINQ)は打ち切り後の無駄な検索は実行しない(即時実行では、必ず最後まで検索を実行してしまう)。

 つまり、メモリ効率/処理効率の点から、検索結果のコレクションを得るにはLINQの方が優れているのである(後続の処理がforeach(C#)/For Each(VB)ループの場合)。

最初に見つかったものだけを得る例

 条件を満たす要素のうちで最初に見つかったものだけがほしい場合は、LINQのFirstOrDefault拡張メソッドを使う。List<T>クラスのFindメソッドを使ってもよい(次のコード)。なお、見つからなかった場合は、どちらも型の既定値が返る(整数では0)。

List<int> list = new List<int> {1,2,3,4,5,6,7,8,9,10,};

// 偶数を検索して、最初に見つかったものだけを得る
// 【1】LINQのFirstOrDefault拡張メソッド
int result1 = list.FirstOrDefault(n => n % 2 == 0);
WriteLine(result1);
// 出力:2

// 【2】List<T>クラスのFindメソッド
int result2 = list.Find(n => n % 2 == 0);
WriteLine(result2);
// 出力:2

Dim list As List(Of Integer) _
  = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

' 偶数を検索して、最初に見つかったものだけを得る
' 【1】LINQのFirstOrDefault拡張メソッド
Dim result1 As Integer = list.FirstOrDefault(Function(n) n Mod 2 = 0)
WriteLine(result1)
' 出力:2

' 【2】List<T>クラスのFindメソッド
Dim result2 As Integer = list.Find(Function(n) n Mod 2 = 0)
WriteLine(result2)
' 出力:2

最初に見つかったものだけを得る例(上:C#、下:VB)

条件を満たす要素が含まれているかどうかを調べる例

 条件を満たす要素の有無だけを知りたい場合は、LINQのAny拡張メソッドを使う。List<T>クラスのExistsメソッドを使ってもよい(次のコード)。

List<int> list = new List<int> {1,2,3,4,5,6,7,8,9,10,};

// 偶数が含まれているかどうかを調べる
// 【1】LINQのAny拡張メソッド
bool result1 = list.Any(n => n % 2 == 0);
WriteLine(result1);
// 出力:True

// 【2】List<T>クラスのExistsメソッド
bool result2 = list.Exists(n => n % 2 == 0);
WriteLine(result2);
// 出力:True

Dim list As List(Of Integer) _
  = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

' 偶数が含まれているかどうかを調べる
' 【1】LINQのAny拡張メソッド
Dim result1 As Boolean = list.Any(Function(n) n Mod 2 = 0)
WriteLine(result1)
' 出力:True

' 【2】List<T>クラスのExistsメソッド
Dim result2 As Boolean = list.Exists(Function(n) n Mod 2 = 0)
WriteLine(result2)
' 出力:True

条件を満たす要素が含まれているかどうかを調べる例(上:C#、下:VB)

特定の要素が含まれているかどうかを調べる例

 特定の要素の有無だけを知りたい場合は、Containsメソッドを使う(次のコード)。

 ただし、Containsという名前のメソッドはLINQ拡張にもList<T>クラスにもある。引数が1つだけのものは、C#では型引数の有無で区別される(他に、LINQ拡張のContainsメソッドには引数が2つのものもある)。VBでは区別できないので、LINQ拡張のContainsメソッドを明示的に呼び出すには次のコードのようにEnumerableクラスの静的メソッドとして呼び出さねばならない。だが、実用上は、LINQ拡張のContainsメソッドを使っているつもりで「list.Contains(……)」と書いてよい(次のコードの解説を参照)。

List<int> list = new List<int> {1,2,3,4,5,6,7,8,9,10,};

// 特定の要素「5」が含まれているかどうか調べる
// 【1】LINQのContains<T>拡張メソッド
bool result1 = list.Contains<int>(5);
WriteLine(result1);
// 出力:True

// 【2】List<T>クラスのContainsメソッド
bool result2 = list.Contains(5);
WriteLine(result2);
// 出力:True

Dim list As List(Of Integer) _
  = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

' 特定の要素「5」が含まれているかどうか調べる
' 【1】LINQのContains<T>拡張メソッド
Dim result1 As Boolean = Enumerable.Contains(list, 5)
WriteLine(result1)
' 出力:True

' 【2】List<T>クラスのContainsメソッド
Dim result2 As Boolean = list.Contains(5)
WriteLine(result2)
' 出力:True

特定の要素が含まれているかどうかを調べる例(上:C#、下:VB)
ContainsメソッドはLINQ拡張にもList<T>クラスにもある。どちらを使えばよいのだろうか? それは悩まなくてもよいのだ。なぜなら、どちらも最終的にはList<T>クラスのContainsメソッドを呼び出すからだ。List<T>クラスは、ICollection<T>インタフェースを実装している。そして、LINQのContains<T>拡張メソッドは、対象のコレクションがICollection<T>インタフェースを実装しているときはそのコレクション(=今の場合はList<T>コレクション)のContainsメソッドを呼び出すのである(MSDN「Enumerable.Contains(Of TSource) メソッド」の解説を参照)。
このサンプルコードのように明示的にLINQのContains拡張メソッドを呼び出すコードを書いても、結局はList<T>クラスのContainsメソッドが呼び出される。シンプルに「list.Contains(……)」とコーディングすればよい。

要素数が多いときに威力を発揮するBinarySearch

 List<T>クラスにはBinarySearchメソッドがある(LINQ拡張にはない)。これは要素数が多いときに高速に検索できるメソッドだ。List<T>クラスのFindメソッドやLINQのFirstOrDefault拡張メソッドの計算量はO(n)なのに対して、BinarySearchメソッドではO(log n)である。ただし、あらかじめ要素が昇順に並んでいなければならないので、使いどころが難しい(1回の検索のためにソートしていたのではかえって時間がかかってしまう可能性が高い)。

 次のコードに、要素が昇順に並んでいてうまく検索できる例と、昇順に並んでいないために検索に失敗する例を示す。

// 特定の要素「5」を検索する

List<int> list = new List<int> {1,2,3,4,5,6,7,8,9,10,};
// 「list」は昇順に並んでいるので、BinarySearchメソッドが使える
int index = list.BinarySearch(5);
WriteLine($"index={index}, value={list[index]}");
// 出力:index=4, value=5

List<int> list2 = new List<int> { 1, 2, 3, 4, 9, 6, 7, 8, 5, 10, };
// 「list2」は昇順に並んでいないため、正しい結果が得られない
int index2 = list2.BinarySearch(5);
WriteLine($"index2={index2}");
// 出力:index2=-5

' 特定の要素「5」を検索する

Dim list As List(Of Integer) _
  = New List(Of Integer) From {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
' 「list」は昇順に並んでいるので、BinarySearchメソッドが使える
Dim index As Integer = list.BinarySearch(5)
WriteLine($"index={index}, value={list(index)}")
' 出力:Index=4, value=5

Dim list2 As List(Of Integer) _
  = New List(Of Integer) From {1, 2, 3, 4, 9, 6, 7, 8, 5, 10}
' 「list2」は昇順に並んでいないため、正しい結果が得られない
Dim index2 As Integer = list2.BinarySearch(5)
WriteLine($"index2={index2}")
' 出力:index2=-5

BinarySearchメソッドが成功する例と失敗する例(上:C#、下:VB)
BinarySearchメソッドは検索に成功すると、見つかった要素のインデックスを返す。失敗したときは負のインデックスを返す。要素が昇順に並んでいないときは検索に失敗する。

まとめ

 List<T>コレクションの要素を検索するときも、他のコレクションと同様にLINQ拡張を使えばよい。ただし、要素数が非常に多い場合はList<T>クラスのBinarySearchメソッドの利用を検討しよう。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。