LINQを使って文字列コレクションでOR検索を行うには、いくつかの方法がある。本稿ではそれらの方法を示しながら、長所短所について検討する。
対象:.NET 3.5以降
LINQを使って文字列のコレクションを処理するとき、OR検索をしたいことがあるだろう。あらかじめ条件が決まっているならば、複雑な条件式であってもそのままWhereメソッド(System.Linq名前空間のEnumerableクラスに定義された拡張メソッド)に渡すラムダ式に記述すれば済む*1。しかし、例えばエンドユーザーからの入力を基にして検索を実行するような場合には、ORでつなぐ条件の数が動的に変化する。そのような場合はどうしたらよいだろうか? 本稿ではその方法を説明する。
あらかじめお断りしておくが、AND/ORやかっこが入り混じった本当に複雑な条件の場合には、ラムダ式を動的に組み立てて式ツリーを生成することになる。そのような複雑な検索条件では、構文解析が必須であろう。構文解析を行うなら、そのついでに式ツリーを生成するのはそれほど困難なことではない。式ツリーを生成する方法は、「.NET TIPS:LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB]」を参照してほしい。
本稿では、ORだけの条件式を考える。検索機能を実装するときに、空白で区切られた語句を全てOR条件として扱うような簡易的な検索方法を想定している。また、「.NET TIPS:LINQ:文字列コレクションで『LIKE検索』(部分一致検索)をするには?[C#、VB]」で紹介したような「LIKE検索」では考慮事項が増えてしまうので、本稿では文字列中にキーワードを含んでいるかどうかの比較だけとする(比較にはSystem名前空間のStringクラスのContainsメソッドだけを使う)。
本稿では、Whereメソッドが実際どのように動作するのかを確認したい。OR検索ならばショートサーキット(結果が真に確定した時点で後続の条件比較を打ち切る)してほしいものである。それを検証するために、比較内容をコンソールに出力する「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メソッドを用いる。
あらかじめOR条件の数が決まっているときは、そのまま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("OR検索0: ラムダ式中でOR条件");
// LIKE '%ぶた%' OR LIKE '%まつり%'
IEnumerable<string> OR検索0
= sampleData.Where(item => item.ContainsEx("ぶた") || item.ContainsEx("まつり"));
WriteItems("OR検索0", OR検索0);
// → OR検索0: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
#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("OR検索0: ラムダ式中でOR条件")
' LIKE '%ぶた%' OR LIKE '%まつり%'
Dim OR検索0 As IEnumerable(Of String) _
= sampleData.Where(Function(item) item.ContainsEx("ぶた") OrElse item.ContainsEx("まつり"))
WriteItems("OR検索0", OR検索0)
' → OR検索0: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
#If DEBUG Then
Console.ReadKey()
#End If
End Sub
End Module
これを実行してみると、次のような結果になる。
ところで、ORでつなぐ条件の数があらかじめ分かっていないときは、このようにハードコーディングすることはできない。そのようなときにはどうすればよいかが、本稿の主題である。
AND検索の場合には「.NET TIPS:LINQ:文字列コレクションで複数キーワードのAND検索をするには?[C#、VB]」で述べたようなWhereメソッドをチェーンするという汎用的な解法があった。しかし、LINQでOR検索する場合には汎用的な解法がないのである(後述するように一長一短があり、「これだけを覚えておけばOK!」とはいかない)。
先に挙げた例のような同じ条件式(=「item.ContainsEx("{キーワード}")」)をORでつなぐ場合では、Anyメソッド(System.Linq名前空間のEnumerableクラスに定義された拡張メソッド)を利用するとすっきり書ける(次のコード)。
Console.WriteLine();
Console.WriteLine("OR検索1: Anyメソッドを使う");
// Anyメソッドを利用してOR検索をする
string[] keywords = { "ぶた", "まつり", };
IEnumerable<string> OR検索1
= sampleData.Where(item => keywords.Any(key => item.ContainsEx(key)));
WriteItems("OR検索1", OR検索1);
// → OR検索1: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
Console.WriteLine()
Console.WriteLine("OR検索1: Anyメソッドを使う")
' Anyメソッドを利用してOR検索をする
Dim keywords As String() = {"ぶた", "まつり"}
Dim OR検索1 As IEnumerable(Of String) _
= sampleData.Where(Function(item) keywords.Any(Function(key) item.ContainsEx(key)))
WriteItems("OR検索1", OR検索1)
' → OR検索1: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
実行結果は次の画像のようになる。期待通りショートサーキット評価になっている。
同じ条件式をORでつなぐ場合には、シンプルに書けるこの方法がよいだろう。
なお、同じ条件式をANDでつなぐ場合にはAllメソッド(System.Linq名前空間のEnumerableクラスに定義された拡張メソッド)を利用して次のように書くこともできる。
Console.WriteLine();
Console.WriteLine("AND検索3: Allメソッドを使う");
IEnumerable<string> AND検索3
= sampleData.Where(item => keywords.All(key => item.ContainsEx(key)));
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", AND検索3);
// → LIKE '%ぶた%' AND LIKE '%まつり%': ねぶたまつり
Console.WriteLine()
Console.WriteLine("AND検索3: Allメソッドを使う")
Dim AND検索3 As IEnumerable(Of String) _
= sampleData.Where(Function(item) keywords.All(Function(key) item.ContainsEx(key)))
WriteItems("LIKE '%ぶた%' AND LIKE '%まつり%'", AND検索3)
' → LIKE '%ぶた%' AND LIKE '%まつり%': ねぶたまつり
このように、Allメソッド/Anyメソッドを利用すると、複数の条件式をAND/ORでつないだ検索をすっきりと記述できる。しかし、異なる条件式が混在するときには使えないので注意してほしい。例えば次のような場合だ。
// 異なる条件式をANDでつなぐ例(これだけでは実行できない)
books.Where(book => book.著者.Contains("夏目") && book.タイトル.Contains("猫"));
' 異なる条件式をANDでつなぐ例(これだけでは実行できない)
books.Where(Function(book) book.著者.Contains("夏目") AndAlso book.タイトル.Contains("猫"))
異なる条件式が混在するOR検索の場合は、どうしたらよいだろうか? OR検索は、個別に絞り込んだ結果をマージしても同じ結果になる。そのコードと実行結果を次に示す。
Console.WriteLine();
Console.WriteLine("OR検索2a: 別々にWhereしてUnion");
// 個別に絞り込んだ結果をマージすればOR検索になる
IEnumerable<string> OR検索2a = new List<string>(); // 空のコレクションを用意
OR検索2a = OR検索2a.Union(sampleData.Where(item => item.ContainsEx("ぶた"))); // 検索して結果をマージ
OR検索2a = OR検索2a.Union(sampleData.Where(item => item.ContainsEx("まつり"))); // 検索して結果をマージ
WriteItems("OR検索2a", OR検索2a);
// → OR検索2a: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
Console.WriteLine()
Console.WriteLine("OR検索2a: 別々にWhereしてUnion")
' 個別に絞り込んだ結果をマージすればOR検索になる
Dim OR検索2a As IEnumerable(Of String) = New List(Of String)() ' 空のコレクションを用意
OR検索2a = OR検索2a.Union(sampleData.Where(Function(item) item.ContainsEx("ぶた"))) ' 検索して結果をマージ
OR検索2a = OR検索2a.Union(sampleData.Where(Function(item) item.ContainsEx("まつり"))) ' 検索して結果をマージ
WriteItems("OR検索2a", OR検索2a)
' → OR検索2a: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
以上のように、個別に絞り込んだ結果をマージしてOR検索とするのは、ショートサーキット評価にならない。さらに、一つのWhereメソッドごとにUnionメソッドを一度実行するコストも大きい(この例では2回だが、検索キーワードが増えたらそれだけUnionメソッドの実行回数も増える)。Whereメソッドをチェーンできないと、効率が悪いのである。
何とかWhereメソッドのチェーンにできないだろうか? AND検索なら、条件式が異なる場合でもチェーンで書けるのだ。そこで、ド・モルガンの法則を使ってORをANDに変えてみよう。ド・モルガンの法則は、一般には次の形で示される。
!(P || Q) == !P && !Q
この両辺をともに否定しても同じである。
!!(P || Q) == !(!P && !Q)
さらに左辺を展開する。
P || Q == !(!P && !Q)
すなわち、それぞれの条件を否定してAND検索し、その結果をまた否定することで、OR検索をしたことになるのである。そのコードと実行結果を次に示す。
Console.WriteLine();
Console.WriteLine("OR検索2b: ド・モルガンの法則");
// ド・モルガンの法則を使ってOR検索をメソッドチェーンにする
IEnumerable<string> NotAnd検索2
= sampleData.Where(item => !item.ContainsEx("ぶた"))
.Where(item => !item.ContainsEx("まつり")); // 否定のANDを取り
IEnumerable<string> OR検索2b
= sampleData.Except(NotAnd検索2); // 最後に元のコレクションから除外する(=否定)
WriteItems("OR検索2b", OR検索2b);
// → OR検索2b: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
Console.WriteLine()
Console.WriteLine("OR検索2b: ド・モルガンの法則")
' ド・モルガンの法則を使ってOR検索をメソッドチェーンにする
Dim NotAnd検索2 As IEnumerable(Of String) _
= sampleData.Where(Function(item) Not item.ContainsEx("ぶた")) _
.Where(Function(item) Not item.ContainsEx("まつり")) ' 否定のANDを取り
Dim OR検索2b As IEnumerable(Of String) _
= sampleData.Except(NotAnd検索2) ' 最後に元のコレクションから除外する(=否定)
WriteItems("OR検索2b", OR検索2b)
' → OR検索2b: ぶた, こぶた, ぶたまん, ねぶたまつり, ねぷたまつり
このように、ド・モルガンの法則を使ってOR検索をメソッドチェーンに変形する方法は、そこそこ効率はよいのだがコードが読みにくくなる。
以上のように、Whereメソッドの組み合わせでOR検索を実現する方法には一長一短がある。Anyメソッドの利用を先に検討してみるのがよいだろう。
LINQでのOR検索は難しい。同じ条件式をORでつなぐ場合なら、Anyメソッドを利用するとよい。そうでないときは、Unionメソッドを使ったり、ド・モルガンの法則を利用してExceptメソッドを使ったりする。あるいは、本稿では示さなかったが、Whereメソッドのラムダ式で使うための条件判定メソッドを作る場合もある。
*1 Where拡張メソッドの引数には、ラムダ式を与える。ラムダ式について詳しくは、次のMSDNのドキュメントを参照していただきたい。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:クラスライブラリ 処理対象:LINQ
使用ライブラリ:Enumerableクラス(System.Linq名前空間)
使用ライブラリ:Stringクラス(System名前空間)
関連TIPS:LINQ:文字列コレクションで複数キーワードのAND検索をするには?[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.