名古屋合同懇親会 2013忘年会に参加して、急遽LT登壇しました。
(実はまだ進行中です。Twitter #NGK2013B)
後半、ちょっとまくって苦しかった。ウケは取れたので満足です?
ご清聴ありがとうございました。早速、プレゼンおいておきます。
名古屋合同懇親会 2013忘年会に参加して、急遽LT登壇しました。
(実はまだ進行中です。Twitter #NGK2013B)
後半、ちょっとまくって苦しかった。ウケは取れたので満足です?
ご清聴ありがとうございました。早速、プレゼンおいておきます。
前回、.NETにはQueryableクラスがあり、Enumerableクラスの代わりに使用されることで、クエリの条件式をExpressionクラス経由で動的に判断できることを示した。
Queryableクラスには、Enumerableクラスで定義されている様々な拡張メソッドが大体同じように定義されている。今までの連載で、LINQ 拡張メソッドの実装方法例を数多く紹介してきた。Queryableクラスの拡張メソッドは、IQueryableを拡張するという違いを除けば、IEnumerableの拡張メソッドと殆ど変わる事は無い。
本当にそうだろうか?
まず第一に、Funcなどのデリゲートで受けていた条件式は、Expressionクラスで受ける必要があるはずだ。そして、例えば拡張する対象がLINQ to SQLなら、SQL文として成立させなければならない。独自の拡張メソッドを、IQueryableインターフェイス向けに実装したとしよう:
public static class LinqExtensions { // 要素の個数を数える(LINQ to Objects) public static IEnumerable<T> QCount<T>( this IEnumerable<T> enumerable, Func<T, bool> predict) { var count = 0; foreach (var value in enumerable) { // 条件が一致する場合だけカウントする if (predict(value) == true) { count++; } } return count; } // 要素の個数を数える(LINQ to SQL / LINQ to Entities) public static IQueryable<T> QCount<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predict) { var count = 0; foreach (var value in queryable) { // 条件が一致する場合だけカウントする if (predict(value) == true) // <-- 構文エラー { count++; } } return count; } }
このコード例には2つの問題がある。第一に、IQueryableのバージョンでは、要素数を数える処理がSQL文になっていないことだ。LINQ to SQLやLINQ to Entitiesでは、クエリの結果がSQL文としてSQL Server等のRDBシステム上で実行されなければならないが、この実装は手元のCLR上でループを実行しているだけで、見ての通りSQL文はどこにも存在しない。
第二に、条件式を示すpredictがデリゲートではなくExpressionであるため、直接呼び出すことは出来ない(構文エラーとなる)。Expressionは自分で解析して、式がどのようになっているかを把握しなければならない。このQCountをSQL文で表現したとすると、以下のようになるはずだ。
// QCountがSQL文に変換されたとして想定される、擬似的なSQL文 SELECT COUNT(*) FROM [Customer] WHERE predict(row) = TRUE
この擬似SQL文を見ると更に大きな問題がある事が分かる。このSQL文がSQL Serverに送信されて実行される時、predictの呼び出しで、どこのメソッドが呼び出されるのだろうか?.NET側に記述したラムダ式が実行されるのだろうか?SQL Serverからのリモートコールバックで実行されるのか?それはそれは壮大な仕掛けだ。
… そんなわけはない。
第一、仮にそのように動作したとしても、毎行のWHERE評価の度にコールバック呼び出しが発生する事になる。非現実的な遅さだ。では実際にはどこで実行されるのだろうか?
この答えは、「そもそも想定が間違っている」だ。まず、predictはデリゲートではないので、Expressionを自力で評価する必要がある。「評価した式がSQL文に変換可能」であれば、SQL文としてRDBに送信出来ることになる。変換不能であれば、クエリを実行する事は出来ない。それは「実行時例外」という形で表現されることになる。例を示そう:
// 正規表現でマッチングを試みる var regex = new Regex("[a-zA-Z]*"); // LINQ to SQLのコンテキスト using (var context = new AdventureWorksDataContext()) { // LINQ to Objectsなら問題ないが? var customers = from customer in context.Customer where regex.IsMatch(customer.FirstName) == true // 正規表現にマッチすれば select customer; }
Regexクラスは、当然.NET固有のクラスだ。Regex.IsMatchに相当するSQL構文は存在しない。だからこのLINQクエリは、SQL文で「表現できない」。表現できないので、実行しようとすると例外が発生する。
つまり、IQueryableの拡張メソッドは、そのメソッドが意味するところをSQL文に変換出来なければならない。或は逆だ。SQL文で表現可能であれば、その意味するところを拡張メソッドに出来る可能性がある、と。
前回、ラムダ式をExpressionクラスとして扱う例を示した。一体これの何がLINQと関係あるのかと思うかもしれない。
ところで、LINQクエリをクエリ構文で書き、それをメソッド構文と対比させて何が違うのかを考えたことがあるだろうか?
// 乱数から偶数だけを抜き出す var r = new Random(); // クエリ構文 var resultsByQueryStatement = from index in Enumerable.Range(0, 10000) let randomValue = r.Next() where (randomValue % 2) == 0 select randomValue; // メソッド構文 var resultsByMethodStatement = Enumerable.Range(0, 10000). Select(index => r.Next()). Where(randomValue => (randomValue % 2) == 0);
どちらも同じ結果が得られる(乱数であることを除けば)。
より具体的にクエリ構文を見てみると、where句の式「(randomValue % 2) == 0」は、メソッド構文のWhere拡張メソッドと対比する事が分かる。但し、メソッド構文ではラムダ式で記述しなければならない。単なる式だけでは、結果はbool値になってしまうからだ。Where拡張メソッドの定義は:
public static class Enumerable { // 第二引数のpredictはデリゲート public static IEnumerable<T> Where<T>( this IEnumerable<T> enumerable, Func<T, bool> predict); }
となっていて、第二引数(拡張メソッド呼び出しでは、第一引数相当)はデリゲートを渡さなければならない。このデリゲートによって、列挙子enumerableの一つ一つの要素が(呼び出し側のラムダ式によって)評価されて抽出が実現する。式を評価するので、bool値を渡しても意味がないわけだ。
ではここで唐突だが、QueryableクラスのWhereメソッドを見てみよう。
// System.Linq.Queryableクラス(.NET標準クラス) public static class Queryable { // 第二引数のpredictはExpressionクラス public static IEnumerable<T> Where<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predictExpression); }
Queryableクラスは、IQueryableインターフェイスに対するLINQ拡張メソッド群を定義している。このインターフェイスはIEnumerableも継承しているので、通常のLINQクエリ(LINQ to Objects)として使うこともできる。が、普通、LINQ to Objectsを使うときには、このインターフェイスを使わない。どこで使うのだろうか?
その答えが、LINQ to SQLのDataContextや、LINQ to EntitiesのDbContextクラスだ。より正確には、これらのクラス(ウィザードで生成した継承クラス)に定義されている、テーブルを模したプロパティ(AdventureWorksDataEntities.Customerなど)が、IQueryableインターフェイスを実装した型なのだ。だから:
// varを型に置き換えると using (AdventureWorksLT2012_DataEntities context = new AdventureWorksLT2012_DataEntities()) { // LINQクエリ式の結果はIQueryable<Customer> IQueryable<Customer> customers = from customer in context.Customer // Customerプロパティは、DbSet<Customer> where customer.LastName == "Johnson" orderby customer.FirstName descending select customer; Debug.WriteLine(customers.ToString()); }
CustomerプロパティはDbSet<Customer>クラスだが、このクラスはIQueryable<Customer>を実装している。そのため、以降のクエリは全てQueryableクラスの拡張メソッド群が使用され、最終的な式もIQueryable<Customer>となる。
もう一度、Queryable.Where()の第二引数を見ると、「Expression<Func<T>>」となっている。そのため、where句で書いたラムダ式「customer.LastName == “Johnson”」は、デリゲートとして渡されるのではなく、Expressionとして渡されるのだ。
まとめると、DataContextやDbContextを使用すると、Enumerableクラスの実装ではなく、Queryableクラスの拡張メソッドが呼び出され、ラムダ式で記述した絞り込み条件(他、orderbyやselectの式も同様)は、デリゲートではなくExpressionクラスで渡されることになる。どうしてそうなっているのか?
もう見えてきたかもしれない。Expressionで渡されれば、記述した式そのものの構造を解析する事が可能となる。前回、プロパティを参照するラムダ式をExpressionで取得して、ここからプロパティ名を取得する方法を紹介した。同様に、Whereに渡された絞り込み条件のラムダ式から、絞り込み条件として記述した式の構造そのものにアクセスする事が可能となる。
式で使用したプロパティ名はもちろん、比較式(上記の例では==による文字列との比較や、比較対象の”Johnson”という文字列)も抽出する事が可能だ。もっと複雑な、括弧がネストしたり、複数のプロパティを参照したり、メソッドを呼び出したり、これらを複雑に組み合わせた式も、Expressionから解析する事が出来る。
このような方法で、最終的にExpressionクラスからSQL文を構築し、SQL Serverに送信して実行させているのだ。だから、LINQ to Objectsのように、クエリ結果が大量にインメモリで処理されるという心配は無用だし、基本的にLINQクエリで記述したクエリの効率は、生のSQL文字列で(SqlConnectionやSqlCommandを使って)実行したものと変わらない事になる。
そして、LINQクエリは文字列に頼らないのでタイプセーフだ。前回示したように、リファクタリングツールでも意味が解析されて正しい箇所だけが更新される。SQL Serverのテーブルから生成していれば、エンティティクラスの型定義も間違えようがない。SqlDataReaderでちまちまと型変換しながらレコードを処理する邪魔なコードを書かなくていい。こういった、本来人間がやるべきではない作業を自動化する事が出来るようになる。
デバッグやフレームワークのサポートメソッドとして、動的にプロパティ名を取得したい場合がある。例えば、エラーが発生した場合に、そのエラーが発生したプロパティ名をログに記録したいとする。
private ILog log_ = ...; private string description_; // 詳細内容を取得・設定する(スペースは含まれてはならない) public string Description { get { return description_; } set { // スペースが含まれていれば if (value.Contains(" ") == true) { // ログにエラーを記憶する(Log4Net風) log_.Error("Description"); // <-- プロパティ名 return; } description_ = value; } }
ログ出力時に、プロパティ名を文字列として渡している。このコードを実装後、「Description」というプロパティ名が不適切であり、「Title」に変更したいとしよう。Visual Studioにはテキスト一括置換が存在するので、DescriptionからTitleに一括置換する方法が考えられる。しかし、この方法は他のコードでたまたまDescriptionという名前を持つプロパティやメソッド、更にはDescriptionという文字列なども含め、あらゆるDescriptionをTitleに変更してしまう。これでは、正しく置換されたかどうかを検証しなければならない。
そのようなわけで、Visual Studioには「リファクタ」機能があり、安全にシンボル名を変更する事が出来る。変更しようとしているシンボル名を意味的に解析し、単に同じ名前というだけでなく、実際にコードとして同じ対象を参照するシンボル名だけを、正しく置換してくれる。この機能を使って、上記のDescriptionをTitleに置き換えると、以下のようになる。
public sealed class PersonEntity { private ILog log_ = ...; private string title_; // <-- title_に変更 // Titleに変更 public string Title { get { return title_; } set { // スペースが含まれていれば if (value.Contains(" ") == true) { // ログにエラーを記憶する(Log4Net風) log_.Error("Description"); // <-- プロパティ名 return; } title_ = value; } } }
最初にプライベートフィールドの「description_」を「title_」にリファクタする。すると、ソースコード上のすべてのdescriotion_がtitle_に置き換わる。一括置換でも同じ結果が得られるが、こちらの方がより安全に修正出来ることは、上で説明したとおりだ。
次に、プロパティの名前「Description」を「Title」にリファクタする。ここで問題が発生する。Descriptionプロパティを参照している箇所(上記には存在しないが、他のメソッドやクラスなどから参照している場合は、それらのシンボル名)は、リファクタ操作で正しく修正される。しかし、ログ出力を行っているErrorメソッドの引数の”Description”という文字列は修正されない。
リファクタ操作では、コードの意味が解析される。プロパティやメソッドへの参照コードであれば、自動的に追跡されて修正されるが、文字列である場合は意味的に同一なのかどうかを機械的に判定する事が出来ない。そのため、リファクタでは文字列は置換対象とならないのだ。
このようなコードが実装の中に散見するようになると、リファクタ機能を使って積極的に安全に修正を行うことが困難となる。そこで、コードに意味を与え、リファクタ対象のシンボル名として認識させつつも、動的にシンボル名を取得する方法を考える必要がある。
まず、ログ出力のコードを独立したメソッドにする。
// ログを出力する private void WriteErrorLog(string propertyName) { log_.Error(propertyName); } public string Title { get { return title_; } set { // スペースが含まれていれば if (value.Contains(" ") == true) { // ログにエラーを記憶する this.WriteErrorLog("Description"); // <-- プロパティ名 return; } title_ = value; } }
やっていることは変わらない。次が肝心だ。WriteErrorLogの引数を、以下のように変更する。
// ログを出力する(タイプセーフ版) private void WriteErrorLog<T>(Expression<Func<PersonEntity, T>> lambdaExpression) { log_.Error(/* TODO: propertyName */); }
Expressionクラスを受け取るようになっている。Expressionクラスとは、「式」の構造を表したインスタンスだ。例えば、このメソッドは以下のように使用する。
public string Title { get { return title_; } set { // スペースが含まれていれば if (value.Contains(" ") == true) { // ログにエラーを記憶する this.WriteErrorLog(this_ => this_.Title); // <-- プロパティを参照するラムダ式 return; } title_ = value; } }
WriteErrorLogにラムダ式を記述している。ラムダ式は暗黙の型変換が成立する場合は、デリゲートに置き換えることができる。従って、通常は以下のようなデリゲート型(Func<T, U>)の変数で受ける。
// 匿名デリゲート(C# 2.0) Func<PersonEntity, string> propertyNameFunc = delegate() { return this_.Title; } // ラムダ式(C# 3.0) Func<PersonEntity, string> propertyNameFunc = this_ => this_.Title;
しかし、ラムダ式で記述している場合に限り、Expressionクラスでも受けることができる。
// ラムダ式ツリー(C# 3.0) Expression<Func<PersonEntity, string>> lambdaExpression = this_ => this_.Title;
表面的には、Expressionクラスで受けていること以外に違いはない。しかし、Expressionクラスは以下のようにデリゲートを実行する事が出来ない(そもそもExpressionクラスはデリゲートではない)。
// 匿名デリゲート(C# 2.0) var title = propertyNameFunc(); // ラムダ式(C# 3.0) var title = propertyNameFunc(); // ラムダ式ツリー(C# 3.0) var title = lambdaExpression(); // <-- 構文エラー(デリゲートではない)
Expressionクラスが何者なのかはさておき、このクラスを以下のようにすると、ラムダ式に記述したプロパティ名を取得する事が出来る。
// ログを出力する(タイプセーフ版) private void WriteErrorLog<T>(Expression<Func<PersonEntity, T>> lambdaExpression) { // ラムダ式の式部分を、メンバ照会式として取得する var memberExpression = (MemberExpression)lambdaExpression.Body; // メンバ照会式が参照するメンバ情報を、プロパティ情報として取得する var propertyInfo = (PropertyInfo)memberExpression.Member; // プロパティ名を取得する log_.Error(propertyInfo.Name); }
これで、WriteErrorLogメソッドの引数をラムダ式で記述可能となった。ラムダ式でプロパティ参照式を書けば、それはコードとして意味のある記述となる。コードの意味が解釈できれば、リファクタ機能の対象として、自動的に修正が可能となる。文字列による緩すぎるシンボル名の参照を排除する事で、コードのメンテナンス性が向上する。
LINQ to SQLやLINQ to Entitiesは、記述したLINQクエリが直接SQL文に変換されて、SQL ServerなどのRDBシステムに発行される。今まで見て来たLINQクエリや拡張メソッドは、LINQ to Objectsと呼ばれ、同じLINQでありながら、その動作は全く異なる。
もし、連載で示してきた独自の拡張メソッドがそのままSQL Serverと連携するなら、以下のような矛盾が考えられる。
LINQ to SQLやLINQ to Entitiesは、もちろん「SQL構文」でクエリを生成し、SQL Server上でクエリが実行され、「その結果だけ」がCLRに返される。LINQ to Objectsだけを知っている状態では、この動作はにわかに信じがたいかもしれない。また、どうしてそのような事が実現出来るのかは、とてもこの連載だけでは説明できないが、とりあえず、実際に生成されるSQL文を確認する事は出来る。
SQL文の確認方法は、LINQ to SQLとLINQ to Entitiesで異なるため、それぞれについて例を示す。
サンプルDB: AdventureWorks LT 2012
LINQ to SQLの場合、データベース接続から「LINQ to SQLクラス」を生成する。すると、dbmlファイルのデザイナーが表示される(中身は空)。
ここに、サーバーエクスプローラーから必要なテーブルをドラッグアンドドロップで落とすと、以下のようにテーブルが表示される(実際はEntityクラス)。
テーブル間に正しく制約条件が定義されていれば、このようにリレーションシップ(矢印)も自動的に定義される。見た目だけでなく、Entitiyクラスの階層構造が自動的にプロパティとして定義されるので、LINQ to SQLやLINQ to Entitiesを使うなら、制約条件を定義する事は重要な作業となる。
データベースファーストな開発であれば、SQL Server Management Studioで、テーブル設計と同時に制約もつければよい。リリースで邪魔なら、最後に制約だけ削除する方法もある。
これで、データベース接続を抽象化するDataContextクラスと、レコードデータを表すEnttiyクラスが生成された。
// dbmlで定義されたDataContextを生成する using (var context = new AdventureWorksDataContext()) { // Customerテーブルから、LastNameが"Johnson"のレコードを抽出し、FirstNameの降順でソートする var customers = from customer in context.Customer where customer.LastName == "Johnson" orderby customer.FirstName descending select customer; // 上記クエリの、実際のSQL文を取得する var sqlText = customers.ToString(); Debug.WriteLine(sqlText); }
この結果、以下のSQL文が得られる。
SELECT [t0].[CustomerID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[CompanyName], [t0].[SalesPerson], [t0].[EmailAddress], [t0].[Phone], [t0].[PasswordHash], [t0].[PasswordSalt], [t0].[rowguid], [t0].[ModifiedDate] FROM [SalesLT].[Customer] AS [t0] WHERE [t0].[LastName] = @p0 ORDER BY [t0].[FirstName] DESC
LINQのfrom句やselect句はもちろんだが、where句で記述した絞り込み条件(”Johnson”はパラメータ化されている)やorderby句で記述したソート条件(もちろん、descendingも)反映されている。
LINQ to Entitiesの場合、「ADO.NET Entity Data Model」から生成する。
使い方はほぼ同等。LINQクエリ自体は全く同じ。
using (var context = new AdventureWorksLT2012_DataEntities()) { var customers = from customer in context.Customer where customer.LastName == "Johnson" orderby customer.FirstName descending select customer; Debug.WriteLine(customers.ToString()); }
以下が出力されたSQL句。
SELECT [Extent1].[CustomerID] AS [CustomerID], [Extent1].[NameStyle] AS [NameStyle], [Extent1].[Title] AS [Title], [Extent1].[FirstName] AS [FirstName], [Extent1].[MiddleName] AS [MiddleName], [Extent1].[LastName] AS [LastName], [Extent1].[Suffix] AS [Suffix], [Extent1].[CompanyName] AS [CompanyName], [Extent1].[SalesPerson] AS [SalesPerson], [Extent1].[EmailAddress] AS [EmailAddress], [Extent1].[Phone] AS [Phone], [Extent1].[PasswordHash] AS [PasswordHash], [Extent1].[PasswordSalt] AS [PasswordSalt], [Extent1].[rowguid] AS [rowguid], [Extent1].[ModifiedDate] AS [ModifiedDate] FROM [SalesLT].[Customer] AS [Extent1] WHERE N'Johnson' = [Extent1].[LastName] ORDER BY [Extent1].[FirstName] DESC
もし、これらのSQL文が本当に実行されているのか疑問であるなら、SQL Server Profilerで実際に確認してみると良い。
LINQは、正式には.NET 3.5 / C# 3.0でサポートされた。しかし、強力な柔軟性と拡張性の高さを、.NET 2.0のプロジェクトでも使いたいと思う事がある。実は、工夫さえすれば、.NET 2.0環境でもLINQを使う事が出来る。
LINQを使うためには、2つの要件を満たす必要がある。
前者の要件に絡む、分かりにくい問題がある。「拡張メソッド」の扱いだ。C# 3.0以上のコンパイラを使えば、拡張メソッド構文は解釈可能だ。但し、拡張メソッドが定義されたクラスは特殊な扱いを受ける。
// 独自実装のEnumerable [Extension] // <-- Extension属性 public static class Enumerable { // 独自実装のWhere [Extension] // <-- Extension属性 public static IEnumerable<T> Where<T>(this IEnumerable<T> enumerable, Func<T, bool> predict) { foreach (var value in enumerable) { if (predict(value) == true) { yield return value; } } } }
C# 3.0以上のコンパイラで拡張メソッドを定義すると、暗黙にExtensionAttribute属性がクラスとメソッドに適用される(上の例ではわざと書いた)。そのため、アセンブリを生成する際にこの拡張メソッドが見つからないと、ビルドに失敗する。当然、.NET 2.0のmscorlib.dllやSystem.dllには存在しないため、以下のようなコードも盛り込んでおく必要がある。
namespace System.Runtime.CompilerServices { // オレオレExtension属性 [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] public sealed class ExtensionAttribute : Attribute { public ExtensionAttribute() { } } }
そして、こまごまとしたクラス(例えば、Func<T>やAction<T>など)と、あなたが使いたい標準的なLINQ拡張メソッド群(Where<T>やSelect<T>やOrderBy<T>等)を用意する必要がある。これらを用意すれば、「ほぼ」C# 3.5以降のLINQを模倣できる。
ほぼ、と書いたのは、どうしても回避出来ない制限が一つだけあるためだ。それは、IEnumerable<T>が、ジェネリック共変性をサポートしていない事による。これによって発生する問題は別の機会に譲るとして、実際問題、大量のLINQ拡張メソッドを自前で準備するのはなかなか難しい。
そして、同じような事を考える人は世の中にも大勢いて、NuGetでパッケージ化されていたりもするので、特に理由が無ければ、このようなパッケージを利用したほうが良いだろう。以下の「linqbridge」は、単一ソースコードのバージョンもあるので、自分のライブラリに容易に組み込みやすいだろう。
linqbridge – Re-implementation of LINQ to Objects for .NET Framework 2.0
NuGet LINQBridge
NuGet LINQBridge (Embedded)
なお、linqbridgeはPLINQをサポートしていない。AsParallel()から始まるPLINQを用意するのは複雑すぎる。自前で実装するのは不可能ではないが、そんな事を考えるならC# 3.0に移行する事を真剣に考えた方が良い。どうしても並列実行を諦めることが出来ないのなら、TPLの自前実装で我慢しよう。Palallel.ForEach()なら、自力でも実装できるはずだ。
LINQクエリ中で使用出来る拡張メソッドを実装するとき、その引数シグネチャはIEnumerable<T>で受ける事になる。
public static class LinqExtensions { // パブリックなデフォルトコンストラクタを持つ型を抽出する public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable) { return from type in enumerable where (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null) // パブリックなデフォルトコンストラクタがある select type; } }
この拡張メソッドを使ってみる。
// mscorlib.dllのすべての型から、生成可能なクラスを抽出する var types = typeof(object).Assembly.GetTypes(); var creatables = types.OfCreatable();
この時、typesはType[]なので、引数のIEnumerable<Type>に合致する。OfCreatableに実装したLINQクエリは以下のように拡張メソッドで書き直せる。
public static class LinqExtensions { // パブリックなデフォルトコンストラクタを持つ型を抽出する public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable) { return enumerable.Where(type => (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null)); // パブリックなデフォルトコンストラクタがある } }
ここで、typesを並列化したらどうなるだろうか。
// mscorlibのすべての型から、生成可能なクラスを抽出する var types = typeof(object).Assembly.GetTypes().AsParallel(); // 並列化 var creatables = types.OfCreatable();
varを書き直すと、以下のようになる。
// mscorlib.dllのすべての型から、生成可能なクラスを抽出する ParallelQuery<Type> types = typeof(object).Assembly.GetTypes().AsParallel(); // 並列化 IEnumerable<Type> creatables = types.OfCreatable();
AsParallel()によって、列挙子の方がParallelQuery<Type>となる。しかし、OfCreatableの引数はIEnumerable<Type>なので、暗黙のキャストが発生し、結局OfCreatable内のLINQクエリは並列化されないまま実行される(以前の連載で述べたように、これは暗黙のゲートだ)。念のため、こういうことだ:
// mscorlib.dllのすべての型から、生成可能なクラスを抽出する ParallelQuery<Type> types = typeof(object).Assembly.GetTypes().AsParallel(); // 並列化 IEnumerable<Type> creatables = LinqExtensions.OfCreatable((IEnumerable<Type>)types); // IEnumerable<Type>に戻してから渡される
では、OfCreatable内のLINQクエリを並列化したい場合はどうすれば良いだろうか?
public static class LinqExtensions { // パブリックなデフォルトコンストラクタを持つ型を抽出する public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable) { return from type in enumerable.AsParallel() // <-- ここで並列化 where (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null) // パブリックなデフォルトコンストラクタがある select type; } }
並列化したいのだから、内部のLINQクエリでもAsParallel()で並列化すればよいと思うかもしれないが、これは微妙だ。引数のところでゲートが出来ていることは解消されない(IEnumerable<Type>になる)ため、OfCreatableを呼び出す直前のクエリが並列クエリであったとしても、一旦並列性が解除されてしまう。
また、戻り値として返される列挙子もIEnumerable<Type>なので、ここでもゲートを作ってしまう。そして、AsParallelがハードコードされたことで、わざと非並列状態で実行させたくても出来ないという問題もある。
これらを解消するには、以下のようにすればよい。
public static class LinqExtensions { // パブリックなデフォルトコンストラクタを持つ型を抽出する(非並列化) public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable) { return from type in enumerable where (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null) // パブリックなデフォルトコンストラクタがある select type; } // パブリックなデフォルトコンストラクタを持つ型を抽出する(並列化) public static ParallelQuery<Type> OfCreatable(this ParallelQuery<Type> parallelEnumerable) { return from type in parallelEnumerable where (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null) // パブリックなデフォルトコンストラクタがある select type; } }
要するに、引数(と戻り値)の列挙子の型が、ParallelQuery<T>となるようなオーバーロードを用意する。すると、中のLINQクエリの記述が全く同一であったとしても、それらの拡張メソッド(WhereやSelect等)は、Enumerable.WhereとParallelEnumerable.Whereのように呼び分けが行われる。結果として、並列バージョンではクエリ全体が並列化され、IEnumerable<T>のような通常の列挙子から呼び出される場合は、非並列クエリとなる。
これで目的を達したのだが、最後にこのメソッドを、非並列バージョンと並列バージョンのクラスに分ける。
// LINQ拡張メソッド群(非並列バージョン) public static class LinqExtensions { // パブリックなデフォルトコンストラクタを持つ型を抽出する public static IEnumerable<Type> OfCreatable(this IEnumerable<Type> enumerable) { return from type in enumerable where (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null) // パブリックなデフォルトコンストラクタがある select type; } } // LINQ拡張メソッド群(並列バージョン) public static class ParallelLinqExtensions { // パブリックなデフォルトコンストラクタを持つ型を抽出する public static ParallelQuery<Type> OfCreatable(this ParallelQuery<Type> parallelEnumerable) { return from type in parallelEnumerable where (type.IsPublic == true) && // パブリックであり、 (type.IsClass == true) && // クラスであり、 (type.IsAbstract == false) && // 抽象クラスではなく (type.GetConstructor(Type.EmptyTypes) != null) // パブリックなデフォルトコンストラクタがある select type; } }
このように、非並列バージョンと並列バージョンの拡張メソッドを分けることで、丁度EnumerableクラスとParallelEnumerableクラスが分けられているのと同じ構造となる。
前回に続き、もう一つ別の例を考える。Enumerable.ElementAt()だ。このメソッドは、列挙子に対してインデックス値で指定された位置の要素を取得する。
public static class LinqExtensions { public static T ElementAt<T>(this IEnumerable<T> enumerable, int index) { var i = 0; foreach (var value in enumerable) { if (i >= index) { return value; } i++; } throw new IndexOutOfRangeException(); } }
これもまた、列挙子を実際に列挙しなければ、目的の値を得ることが出来ない。Listや配列であれば、インデクサアクセスが可能なはずだ。そこで:
public static class LinqExtensions { public static T ElementAt<T>(this IEnumerable<T> enumerable, int index) { // IListにキャスト出来れば var list = enumerable as IList; if (list != null) { // 直接要素を得る(非ジェネリックなのでT型へキャストが必要) return (T)list[index]; } var i = 0; foreach (var value in enumerable) { if (i <= index) { return value; } i++; } throw new IndexOutOfRangeException(); } }
IListを使えば、インデクサによるアクセスが可能だ。これにより、要素を列挙しなくても、任意の位置の値を直接引き出す事が出来る。更に、.NET 4.5からはIReadOnlyListもサポートされるようになったので、このインターフェイスへのキャストも試みると良いだろう。
Enumerable.Count()メソッドの特徴を考えてみる。ただ要素数を数えるだけなら、以下のような実装で良い筈だ。
public static class LinqExtensions { public static int Count<T>(this IEnumerable<T> enumerable) { var count = 0; foreach (var value in enumerable) { count++; } return count; } }
当たり前だが、これでは要素を全て列挙しなければ個数が分からない。例えば:
// 100000000個の値が入ったリスト var values = Enumerable.Range(0, 100000000).ToList(); // 有意に時間がかかる var count1 = LinqExtensions.Count(values); // こちらは一瞬 var count2 = Enumerable.Count(values);
何故、Enumerable.Count()は速いのだろうか。それは、列挙子の実行時の型を見て最適化を行っているからだ。ICollectionインターフェイスを実装しているクラスであれば、Countプロパティを参照するだけで個数が識別出来る。この方法を利用するには、以下のようにキャストで判定する。
public static class LinqExtensions { public static int Count<T>(this IEnumerable<T> enumerable) { // 列挙子がICollectionを実装していれば var collection = enumerable as ICollection; if (collection != null) { // 直接Countプロパティを参照する return collection.Count; } var count = 0; foreach (var value in enumerable) { count++; } return count; } }
列挙子として配列を使用する場合、配列の長さを調べるにはLengthプロパティを参照する。しかし、配列は暗黙にICollectionインターフェイスを実装しているため、わざわざ配列かどうかを確認しなくても、上記コードがそのまま使える。
Regexクラスを使うと正規表現を使って文字列のキャプチャが出来る。多少なりとも簡単に連続判定できるようにしてみる。
public static class LinqExtensions { public static IEnumerable<Match> Match( this IEnumerable<string> sentences, string pattern, RegexOptions options = RegexOptions.Compiled | RegexOptions.Multiline) { var regex = new Regex(pattern, options); foreach (var sentence in sentences) { foreach (Match match in regex.Matches(sentence)) { yield return match; } } } }
これで、引数に正規表現パターンを指定するだけで、連続する文字列に処理が可能となった。
// 文字列中に存在するIPv4アドレスを抽出する var sampleDatas = new[] { "IP:133.0.0.0", "IP:133.255.255.255 primary", "[192.50.0.0]", "IPn192.50.255.255n" }; foreach (var match in sampleDatas.Match(@"(?<1>d+)(?:.(?<1>d+)){3}")) { Console.WriteLine(match); }
上記の例では、ハードコードされた文字列を扱っているが、Advent LINQ (2)で示したTextReader.AsEnumerableと組み合わせれば、LINQクエリ一文でファイルからの正規表現検索が実現する。壮大な時計仕掛けをやってみよう。
// 指定されたフォルダ配下のテキストファイル群からIPv4アドレスを抽出する(並列実行) foreach (var match in from path in Extensions.FilesAsEnumerable(@"C:project", "*.txt").AsParallel() from match in File.OpenText(path).AsEnumerable().Match(@"(?<1>d+)(?:.(?<1>d+)){3}") select match) { Console.WriteLine(match); }
MatchはIEnumerable<string>を受け取るようになっているので、列挙子ではなく単一の文字列を受けるには、1要素の配列を作る必要がある。この部分が分かりにくいのであれば、Match(this string sentence, …) のような、単一の文字列だけを受け取るようなメソッドに変更し、明示的にselect句で射影するようにしてもよい。ただ、その場合は、正規表現をコンパイルするメリットが失われるかもしれない。
参考: .NET Frameworkがサポートする正規表現クラスを徹底活用する