LINQでWhereメソッドをチェーンさせることで、文字列コレクションに対するAND検索を実現する方法を解説。
対象:.NET 3.5以降
LINQを使って文字列のコレクションを処理するとき、AND検索をしたいことがあるだろう。あらかじめ条件が決まっているならば、複雑な条件式であってもそのままWhereメソッド(System.Linq名前空間のEnumerableクラスに定義された拡張メソッド)に渡すラムダ式に記述すれば済む*1。しかし、例えばエンドユーザーからの入力を基にして検索を実行するような場合には、ANDでつなぐ条件の数が動的に変化する。そのような場合はどうしたらよいだろうか? 本稿ではその方法を説明する。
あらかじめお断りしておくが、AND/ORやかっこが入り混じった本当に複雑な条件の場合には、ラムダ式を動的に組み立てて式ツリーを生成することになる。そのような複雑な検索条件では、構文解析が必須であろう。構文解析を行うなら、そのついでに式ツリーを生成するのはそれほど困難なことではない。式ツリーを生成する方法は、「.NET TIPS:LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB]」を参照してほしい。
本稿では、ANDだけの条件式を考える。検索機能を実装するときに、空白で区切られた語句を全てAND条件として扱うような簡易的な検索方法を想定している。また、「.NET TIPS:LINQ:文字列コレクションで『LIKE検索』(部分一致検索)をするには?[C#、VB]」で紹介したような「LIKE検索」では考慮事項が増えてしまうので、本稿では文字列中にキーワードを含んでいるかどうかの比較だけとする(比較にはSystem名前空間のStringクラスのContainsメソッドだけを使う)。
本稿では、Whereメソッドが実際どのように動作するのかを確認したい。AND検索ならばショートサーキット(結果が偽に確定した時点で後続の条件比較を打ち切る)してほしいものである。それを検証するために、比較内容をコンソールに出力する「ContainsEx」メソッドを用意しておく(次のコード)。StringクラスのContainsメソッドと同様に機能するのだが、コンソールに出力する点が異なっている。
using System;
using System.Collections.Generic;
public static class StringExtension
{
// StringクラスのContainsメソッドと同じだが、処理内容をコンソールに書き出すようにした
public static bool ContainsEx(this string s, string key)
{
var result = s.Contains(key);
Console.WriteLine("\"{0}\".Contains(\"{1}\") {2}", s, key, result ? "○" : "×");
return result;
}
}
Imports System.Runtime.CompilerServices
Module StringExtension
' StringクラスのContainsメソッドと同じだが、処理内容をコンソールに書き出すようにした
<Extension()>
Public Function ContainsEx(s As String, key As String) As Boolean
Dim result = s.Contains(key)
Console.WriteLine("""{0}"".Contains(""{1}"") {2}", s, key, If(result, "○", "×"))
Return result
End Function
End Module
以降のコードでは、StringクラスのContainsメソッドの代わりに、このContainsExメソッドを用いる。
あらかじめAND条件の数が決まっているときは、そのままWhereメソッドに記述するだけだ。
例えば、「"ぶた"」と「"まつり"」の両方を含んでいる文字列を検索するコードは次のように書ける。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
// コンソール出力用のメソッド
static void WriteItems(string header, IEnumerable<string> items)
{
var output = string.Join(", ", items.ToArray());
Console.WriteLine("{0}: {1}", header, output);
}
static void Main(string[] args)
{
// サンプルデータ(文字列の配列)
string[] sampleData = { "ぶた", "こぶた", "ぶたまん", "ねぶたまつり",
"ねぷたまつり", "きつね", "ねこ", };
WriteItems("sampleData", sampleData);
Console.WriteLine();
Console.WriteLine("AND検索0: ラムダ式中でAND条件");
// LIKE '%ぶた%' AND LIKE '%まつり%'
IEnumerable<string> AND検索0
= sampleData.Where(item => item.ContainsEx("ぶた") && item.ContainsEx("まつり"));
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", AND検索0);
// → LIKE '%ぶた%' AND LIKE '%まつり%': ねぶたまつり
#if DEBUG
Console.ReadKey();
#endif
}
}
Module Module1
' コンソール出力用のメソッド
Sub WriteItems(header As String, items As IEnumerable(Of String))
Dim output = String.Join(", ", items.ToArray())
Console.WriteLine("{0}: {1}", header, output)
End Sub
Sub Main()
' サンプルデータ(文字列の配列)
Dim sampleData As String() = {"ぶた", "こぶた", "ぶたまん", "ねぶたまつり",
"ねぷたまつり", "きつね", "ねこ"}
WriteItems("sampleData", sampleData)
Console.WriteLine()
Console.WriteLine("AND検索0: ラムダ式中でAND条件")
' LIKE '%ぶた%' AND LIKE '%まつり%'
Dim AND検索0 As IEnumerable(Of String) _
= sampleData.Where(Function(item) item.ContainsEx("ぶた") AndAlso item.ContainsEx("まつり"))
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", AND検索0)
' → LIKE '%ぶた%' AND LIKE '%まつり%': ねぶたまつり
#If DEBUG Then
Console.ReadKey()
#End If
End Sub
End Module
これを実行してみると、次のような結果になる。
ところで、ANDでつなぐ条件の数があらかじめ分かっていないときは、このようにハードコーディングすることはできない。そのようなときにはどうすればよいかが、本稿の主題である。結論を先に述べておくと、ループの中でWhereメソッドを繰り返し呼び出すようにすればよい。なぜそうするのか、順を追って説明していこう。
Whereメソッドで絞り込んだ結果を、さらに絞り込めばAND検索になるはずだ。上に示したコードをWhereメソッドの連鎖(メソッドチェーン)に書き換えてみよう(次のコード)。
Console.WriteLine();
Console.WriteLine("AND検索1: Whereをチェーン");
// WhereメソッドのチェーンでAND検索
IEnumerable<string> AND検索1
= sampleData.Where(item => item.ContainsEx("ぶた"))
.Where(item => item.ContainsEx("まつり"));
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", AND検索1);
Console.WriteLine()
Console.WriteLine("AND検索1: Whereをチェーン")
' WhereメソッドのチェーンでAND検索
Dim AND検索1 As IEnumerable(Of String) _
= sampleData.Where(Function(item) item.ContainsEx("ぶた")) _
.Where(Function(item) item.ContainsEx("まつり"))
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", AND検索1)
このコードは、まず、最初の条件(「"ぶた"」を含むか)を全て判定し、それから2つ目の条件(「"まつり"」を含むか)の判定を行うように見える。最初の条件で絞り込んだコレクションを一時的に作成し、それに対してあらためて2つ目の条件で絞り込んでいるように思える。もしそうならば、先ほどとは比較の順序が変わってくるはずだ(「"ぶた"」→「"ぶた"」→「"こぶた"」→「"こぶた"」……のような順序ではなく「"ぶた"」→「"こぶた"」→……→「"ねこ"」→「"ぶた"」→「"こぶた"」→……のような順序になるはずだ)。
しかし実行してみると、前と同じ結果が得られる(次の画像)。
この実行結果を見ると、予想に反して、最初のWhereメソッドを完全に実行してから、2番目のWhereメソッドを実行するわけではないと分かる(次の図)。LINQの大きな特徴として、Whereメソッドのチェーンは実行時に1つのループにまとめられるのである(Whereメソッドに限らずIEnumerable<T>インターフェースを返す拡張メソッドであれば同様)。
メソッドチェーンは、一時変数に代入して個別の文に分けて書くこともできる。Whereメソッドのチェーンをそのようにして、ループに書き換えてみよう(次のコード)。
// 以前のコード
//IEnumerable<string> AND検索1
// = sampleData.Where(item => item.ContainsEx("ぶた"))
// .Where(item => item.ContainsEx("まつり"));
Console.WriteLine();
Console.WriteLine("AND検索2: Whereするごとに一時変数へ代入してループ化");
string[] keywords = { "ぶた", "まつり", };
IEnumerable<string> work = sampleData;
foreach (var key in keywords)
work = work.Where(item => item.ContainsEx(key));
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", work);
' 以前のコード
'Dim AND検索1 As IEnumerable(Of String) _
' = sampleData.Where(Function(item) item.ContainsEx("ぶた")) _
' .Where(Function(item) item.ContainsEx("まつり"))
Console.WriteLine()
Console.WriteLine("AND検索2: Whereするごとに一時変数へ代入してループ化")
Dim keywords As String() = {"ぶた", "まつり"}
Dim work As IEnumerable(Of String) = sampleData
For Each key In keywords
work = work.Where(Function(item) item.ContainsEx(key))
Next
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", work)
一時変数に代入した時点で中間結果のコレクションが生成され、2つのWhereメソッドが個別に実行されるように思われる。果たしてどうなるであろうか? 実行結果は次の画像のようになる。
最初に示したコード(条件をハードコーディング)の実行結果と、文字列比較の順序は全く同じになった。一時変数に代入して個別の文に分けても、メソッドチェーンのときと同じく、実行時には1つのループにまとめられていると分かる。このようにループにしても、効率よくAND検索が実現できているとお分かりいただけるだろう。
LINQでのAND検索は、Whereメソッドの呼び出しをチェーンすればよい(ループにしてもよい)。Whereメソッドをチェーンするコードは、実行時には自動的に1つにまとめられる(Whereメソッドに限らずIEnumerable<T>インターフェースを返す拡張メソッドであれば同様)
*1 Where拡張メソッドの引数には、ラムダ式を与える。ラムダ式について詳しくは、次のMSDNのドキュメントを参照していただきたい。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:クラスライブラリ 処理対象:LINQ
使用ライブラリ:Enumerableクラス(System.Linq名前空間)
使用ライブラリ:Stringクラス(System名前空間)
関連TIPS:LINQ:文字列コレクションで「LIKE検索」(部分一致検索)をするには?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.