アプリケーションの実行中にコードを生成する:実際の例とテクニック

ランタイムでのコード生成は非常に強力で十分に研究された手法ですが、多くの開発者はまだ使用することに消極的です。 通常、式ツリーの研究は、述語(フィルター)や数式の作成などの簡単な例から始まります。 しかし、Expression Treesだけが生きている.NET開発者ではありません。 最近では、コンパイラ自体を使用してコードを生成することが可能になりました-これは、とりわけ、解析、クロール、およびソース生成を提供するRoslyn / CodeAnalisys APIライブラリを使用して行われます。







この記事は、DotNext 2017モスクワ会議でのRaffaele Rialdi(Twitter: @raffaeler )のレポートに基づいています。 Raphaelと一緒に、コード生成の実際の使用方法を分析します。 場合によっては、アプリケーションのパフォーマンスを大幅に改善できるため、ジレンマに陥ります-生成されたコードが非常に有用で、頻繁に使用する場合、このコードをどのようにデバッグできますか? これは、実際のプロジェクトで発生する基本的な問題の1つです。







Rafaelは、2003年以来、Developer SecurityカテゴリのMVPを抱える実践的なアーキテクト、コンサルタント、スピーカーです。現在、エンタープライズプロジェクトバックエンドに従事し、C#およびC ++のコード生成とクロスプラットフォーム開発に特化しています。









コード生成とは何ですか? パフォーマンスを実証する必要があるとします。 ベンチマークを表示するだけなら、それは一種のトリック、トリッキーなトリックになります。 記事やレポートでは、ベンチマークの表示を避けるべきです-作者にとって危険なためではなく、ベンチマークは1つのシナリオのみを示しており、すべての読者にとって有用ではないためです。 読者は、提案された技術を試して、特定のシナリオに適しているかどうかを判断する必要があります。 したがって、ベンチマークの価値を誇張しないでください。 私自身のために、私はそれらを作ります、彼らはまともな結果を示します。







定義上、リフレクションプログラムはゆっくり実行されることを知っています。 彼女はECMA-335メタデータをダウンロードして解釈する必要があります。 これらは非常にコンパクトなバイナリデータのセットであり、読み取りは非常に複雑です。 アセンブリ後にメモリを大量に消費しないため、コンパクトにする必要があります。 これらのアーティファクトがデプロイされると、非常に低レベルのAPIを扱っているため、パフォーマンスが不十分になります。 ところで、これらすべてのアーティファクトをアセンブリから直接読み込むことで、反射を回避できます。 今日のレポートではこれについては説明しませんが、興味がある場合は、このメソッドを使用して、メモリ内での一定のロードとアセンブリを回避しています。 型情報を除くすべてのものからメモリを解放できます。







コードを正確に生成する必要があるのはいつですか? アルゴリズムを簡素化するのに十分な情報がある場合のアプリケーションライフサイクルのその部分。 これは、たとえば、データベースから取得するレコードの数を減らすフィルターのユーザーインターフェイスから取得できる情報です。 または、プラグインにロードされたタイプに関する情報。 すべての可能なオプションを考慮する一般的なアルゴリズムをリフレクションの助けを借りて作成するのに時間を費やすことは非常に望ましくありません。 開発者は、残念ながら、開発するソリューションを可能な限り一般的にしようとする傾向があり、すべての可能な場合と不可能な場合に取り組んでいます。 私たちのプログラミングマインドにとって、これは自然な思考の流れです。 まったく逆のアプローチをお勧めします。最も簡潔なコードを生成するのに十分な情報が得られるまで、辛抱強く待ちます。







どの場合にコード生成が必要ですか? たとえば、LINQ述語を使用する場合。 述語ビルダーは長い間利用可能です。 または、たとえばExcelの数式を使用する場合。 プラグインからタイプをロードするとき、またはReactive Extensionsを使用するとき。 Reactive Extensionsに慣れている人はどれくらいいますか? これは、データストリームを作成し、グループをフィルタリングしてこのデータを変更できる式を適用できる優れたライブラリです。 反射の力を示すために、これらの例の多くを示します。















C#の式から始めましょう。 この画面は、 Console.WriteLine



呼び出しConsole.WriteLine



生成される簡単なコード例を示しています。 おそらく誰かが尋ねるでしょう-反射を使用することの欠点を指摘しただけで、なぜ反射を使用するのでしょうか? 答えは、一般にリフレクションを放棄するのではなく、コードの最も使用されているセクションからリフレクションを削除することです。 リフレクションを使用して、必要な量のデータを抽出し、コードを生成し、たとえば、コードが実行されるまで待機しないようにループ内で委任を使用できる時点を見つける必要があります。







コードでは、正確なWriteLine



オーバーロードを取得することから始め、その後、入力メッセージになるパラメーターを作成します。 その後、 Call



メソッドに相当するものを作成します。 Expression.Call(null, methodInfo, message)



呼び出しExpression.Call(null, methodInfo, message)



では、 null



は静的メソッドを示します( WriteLine



は静的メソッドです)。 さらに、この呼び出しには、メソッドに関する情報とメッセージを含む引数も必要です。







その後、ラムダが作成されます。 それは非常に簡単で、パラメーターとラムダの本体を指定する必要があります。 すでに作成されたラムダは、非常に便利な.Compile()



メソッドを呼び出します。 メモリ内に直接、非常に簡単な方法で命令を作成するため、優れています。 ソースコードはありません。ドラゴンブックで説明されている方法で処理する必要があるものは何もありません。 コンパイルの最初の段階、つまり、長くて複雑なテキスト分析はありません。 Expressionの場合、構文的に正しいことがすでにわかっているため、これは必要ありません。 これは非常に重要です。 これが、式ツリーが非常にかさばり、非常に不快な厳密な型付けを行う理由です。 すでにいくつかの式を作成しようとしている場合、それが面倒なことを知っています。 しかし、形成された式があるので、実際にコンパイルできます。 コンパイラは、単純にツリーのノード(つまり、特定の式)を取得し、呼び出したいコードに対応するノードを作成します。 最後に、デリゲート、つまり、コードを実行するための最速の手段を形成します。















述語が作成される例を示します。 入力として整数を取り、ブール値を返す非常に単純な関数。 彼女のコードを見てみましょう。 最初の入力値に対して、 Expression.Parameter(typeof(int), "x")



というパラメーターがそこに作成されます。 このメソッドの入力引数の1つは"x"



です。注意しないでください。デバッグにのみ必要です。 変数left



は、式x > -10



の左側、 right



-右側を示します。 これらの2つの変数からバイナリ比較式が作成されます。 最後に、 Lambda



式が返されます。 この場合、必要に応じてデリゲートを変更できるため、デリゲートを返すよりも望ましい方法です。 これを行うには、Visitorパターンを使用できます。これは、式内のすべてのノードを列挙し、非常に正確な方法で変更します。 テキストを操作する必要はありません。すぐに目的のノードに移行します。















いくつかの課題を訪問する必要がある例を挙げます。 コードはLINQで記述されているため、 where



ノードから述語を抽出するとします。 目的の式を取得したら、それにビジターを書くことができます。 where



に拡張メソッドの呼び出しがあるのでwhere



この式を見つけることができます。 最初のwhere



パラメーターはIQueryable<T>



であり、ブール値を返します。 したがって、どのフォームが必要かがわかります。 この式に何かを追加する必要がある場合は、画面上の省略記号が書かれている場所でそれを行うことができます。















退屈しないように、デモに移りましょう。 当初、私は構文解析用のツールを作成したくありませんでした。退屈で、そのようなプログラムは通常遅いことがわかり、書かれたコードよりもこのタスクを実行するライブラリがあります。 小さくて簡単に変更できるものが必要でした。 また、解析ツールを作成する場合、文法を作成する必要があるという事実に気付き、多くのライブラリを使用する必要があります。 さらに、分析後に作成されたノードがエクスプレッションが実際に表現するものに類似するような方法でツールを作成したいと考えました。 その結果、たとえば、テキストの形の式x + y



(コードに表示)を想像し、それを認識しました。















つまり、パラメータを手動で表現しようとしました。 単純化のためにこれを行いましたが、おそらくこれを避けることができます。 Expressionはコンパイルの最初の段階を使用できないため、少なくとも型を指定することが重要です。 たとえば、自動型変換または暗黙的な型変換は使用できません。 integer



からdouble



への変換は使用できません。 これはすべて手動で行う必要があります。







デバッガーの画面に表示されるコードを実行すると、Expressionが返されます。 ラムダはかなり奇妙な方法でVisual Studioデバッガーに表示されますが、それには何の問題もありません。 複雑に見えますが、最終的にはx + y



になります。















私がテキストで書いたSUM()



関数をどのように翻訳できるか見てみましょう。 テキストビジュアライザーは、翻訳結果が現在配置されている変数e



します。 Excelと同じように、定義済みの関数でFunctionsHelper



を定義したことがわかります。 これらの種類のアプリケーションは、関数の一種の語彙を事前に決定する必要があります。 これはすべて非常に簡単です。















コードをもう少し詳しく見てみましょう。 GetFilter()



関数があります。















ご覧のとおり、これはラムダです。 通常、このような場合、 Func<int, bool>



返され、それ以外Func<int, bool>



何も返されません。 ただし、コンパイラには、関数に角かっこがない場合にExpression<Func<int, bool>>



返すことができる特別な機能があります。 つまり、このビューに対して式が自動的に作成されます。 これはまだ変更できるため、非常に便利です。 番号を削除して別の番号に置き換える場合は、式にVisitorを記述し、必要な変更を加えるだけです。















2番目のデモを見てみましょう。 その中には、最初から述語Expression<Func<int, bool>> predicate



ます。







コマンドラインに出力を与えるために、そこにインジェクションを行いたいです。 述語と2つのラムダをインジェクターに渡し、 x



の値を受け取ると、毎回{x} => YES



または{x} => NO



を出力するようx



指定します。 アプリケーションの起動後、 injected



変数がどのように見えるかを見ると、 If



ステートメントを含む関数が表示されます。元の値と比べて大幅に変更されています。















そのため、ここでは整数が入力され、 If



インジェクションが行われると、値に応じてYES



またはNO



がコンソールに出力され、最後に式で処理された値が返されます。 これらの種類のコード変更はすでに実践されており、非常に強力です。







おそらく既に気付いている問題があります-生成されたコードをまだ示したビジュアライザーは、かなり奇妙な形で情報を提示します。 式を使用したプログラミングには特定の利点がありますが、開発者の観点から見ると、コードは「汚い」です。







デモに戻ります。 遅延実行については既に説明しました。数値の列挙が完了するまで、次のコードは実行されません。 今すぐtoList



を取得すると、それらのリストとConsole.WriteLine



両方が取得されます。この場合、これらは自動的に実行されます。























これはすべて良さそうですが、もっと複雑なことを試してみたいと思います。 次の例は夢の中で私に来ました。 コンパイル時に、辞書(おそらくJSON)のデータを特定の順序で変換するラムダを作成します。 タスクは非常に普通です。















リフレクションを使用してこのコードを実行すると、結果は画面に表示されるようになります。















表示プロパティを反復処理し、各プロパティの辞書を照合し、コピーします。 明らかにこのコードは遅いでしょう。 1回しか実行されない場合、これは問題ではありませんが、100万回実行する必要がある場合は理解できます。 サーバーリソースを消費するサーバーアプリケーションでこれが発生した場合、これを好まない人もいます。







別の方法でこの問題を解決してみましょう。 ここでは、 `Orderオブジェクトがコード内に作成され、その要素はクラスに入る辞書に従って設定されます。















値は辞書から抽出され、必要なタイプに変換されてコピーされますが、これはすべて完全に不気味で退屈です。







しかし、すでにOrder



オブジェクトを知っているラムダを作成したらどうなりますか?















このオブジェクトのタイプを示すことが重要です。 <Order>



使用しないことに注意してください。 それは素晴らしいことですが、このタイプがわからない場合はどうでしょうか? 遅延プラグインでOrder



が定義されている場合はどうなりますか? ジェネリックが役立つ場合もありますが、この場合、この情報を無視する必要があるため、それらの使用は望ましくありません。







したがって、コンパイル後にラムダを見てください。















本当に、彼女はいいですか? コードは読みやすいです。 式を使用して生成されました。 ExpressionGeneration



クラスでどのように記述されているかを見てみましょう。















コードは、リフレクションを使用して書いたものに似ていることがわかります。 Expression.Parameter()



が定義され、 result



変数が定義され、 Activator.CreateInstance



を使用して新しいnewEntityType



が作成され、新しいインスタンスがassign変数に割り当てられます。 すべてが非常に退屈です。 次に、 type.getMethod()



type.getMethod()



てメソッドを取得し、その後entityProps



プロパティをentityProps



ます。















この場合、プロパティをいくつ作成するかがわかっているため、この場合はループを作成する必要はありません。 したがって、 callTryGetValue



必要な値を生成するために必要な呼び出しは、ここで生成されます。















次の行はExpression.Convert()



メソッドを呼び出します。型は異なる可能性があるため、それにキャストする必要があります。 次に、プロパティにアクセスするには、 Expression.MakeMemberAccess()



呼び出します。 その後、try-catchコンストラクトに対してExpression.IfThen()



が呼び出されます。 最後に、ブロック、つまり開閉ブラケットが作成されます。 その結果、ラムダが取得されます。















ExpressionsSorcererツールを作成しました。 コードを取得して、 %USERPROFILE%/Visual Studio 2017/Visualizers



ディレクトリに配置し、レビューしたばかりのコードのデバッグを実行できます。 今回は、ビジュアライザーでラムダを見ることができ、ツリーの形で表示されます。















この種の操作は非常に便利で、考えるのに役立ちますが、ここで何を書いているのでしょうか? 別のツリーノードを選択すると、プロパティとその値が右側のウィンドウに表示されます。これは非常に便利です。 「逆コンパイルされたソースを表示」タブを開きます。 私たちの前には、コードジェネレーターに渡された情報があれば書くコードがあります。















しかし、私は指でこのコードに触れませんでした。 C#コードも生成しませんでした。 Expressionsを作成しました。つまり、構文ノードのみが記憶にあり、逆コンパイルする必要がありました。 Roslynのおかげで、ここにも色のマーキングがあり、必要に応じて変更できます。 さらに、コンパイル中に発生する可能性のある最適化が必要ないため、 DebuggableAttribute



属性を追加しました。 あなたはなぜ私がそれらを必要としないのか尋ねるかもしれません そして見返りに、私はあなたのために別の驚きがあります。







( "F11"を押して)デバッグを使用してコンパイルする場合、自動生成されたメソッドに入りますが、このメソッドは自分の手では書きませんでした。 印象的ですね。 ここで、変数の現在の値を確認できます。式のエラーを確認できます。 ご覧のとおり、入力引数にDescription



値がなかったため、 TryGetValue



メソッドが使用されました。















問題の関数の最後に、正しい数の値を持つorder



変数を取得します。







中間結果を要約します。 式は言語のほぼ全体をカバーし、それらを使用してif



throw



catch



生成でき、複雑な構造を作成できます。 しかし、このためには、おそらく、特別なツールが必要です。 私のツールでは、書くのが最も難しい部分は暗黙的な型変換でした。 double x



変数を作成し、 integer



変数に値を割り当てようとすると、 InvalidCastException



ます。 その理由は、暗黙の変換はコンパイラーによって行われますが、それがなかったためです。 したがって、コンパイラが通常行うことのいくつかを処理する必要がありました。







より複雑な式をいくつか紹介します。 画面では、非常に単純なオブジェクトが作成されるコードはvar newObject = ExpressionInterop.BuildNewObject(ctor)



です。















ビジュアライザーでそれを見ると、新しいオブジェクトnew Order()



どのようにnew Order()



かがわかります。















既に述べた理由により、 typeof()



メソッドを使用することを常にお勧めします。 次に、 GetConstructor



メソッドを使用してGetConstructor



必要なコンストラクターを取得し、次にGetMethod



メソッドを介して必要なメソッドを取得します。 その後、コンストラクターに関する情報が送信される新しいオブジェクトExpressionInterop.BuildNewObject(ctor)



が作成されます。 などなど。







これについてはこれ以上詳しく説明しません。 しかし、プロパティに値を割り当てると式がどのように見えるかを示したいと思います...







コンパイルアーティファクトは次のとおりです。























しかし、本当の表現に戻ると、かなり混乱しているように見えます。 私が作成した最も複雑な式の1つは、マーシャリングに使用されます。 AddAsync



非同期コードを実行できるコードを生成しAddAsync



...















...式にTask<T>



表すコードが存在しない場合でも。















コンパイラーMono.Cecilは完全な逆コンパイルを作成できないため、コードは非常に混乱し、再コンパイルできません。 おそらく彼は将来これを行うことができるでしょう。 さらに、ここでの問題は、 Task<int>



外部関数を挿入する必要があることです。 これは、非同期ライブラリの前と、非同期/待機をサポートするためのコンパイラの変更前に式が作成されたためです。 したがって、コンパイラで生成してawaitを使用することはできません。 コンパイラーはすべての魔法を行うので、ILSpyを使用してawaitで作成されたアーティファクトを見ると、そこに継続するコールバックが表示されます。 コードは非常に複雑です。







それで、どこで止めましたか? 式を作成して、特定の述語、関数、if-then-else、throw-catchなどを含むかなり複雑なコードを生成しました。 ロズリンについて話しましょう。















Roslynは、数年前からC#のメインコンパイラとして動作している.NETコンパイラプラットフォームです。 つまり、彼は私たちの世界を支配しています。 かつて私たちにできることはほとんどありませんでしたが、RoslynがAPIをオープンしてくれました。 これで、このコンパイラのAPIを使用して、すべてを直接行うことができます。 書式設定、シンボルに関する情報、さまざまなもののコンパイル、シンボルの解釈、アセンブラの背後のメタデータへの適合などを行うことができます。 カラーマーキングに関しては、Roslynは直接制御しません。 彼は「それは緑でなければならず、そうでなければ青でなければならない」とは示していない。 分析されたトークンの分類があり、さまざまな方法で表示できます。







したがって、多くのツールを利用できますが、問題があります。 ロズリンには強い型付けはありません。 構文ノードがあり、要素は構文ノードであるため、非常に使いやすいです。 ノードを相互に接続することに注意を払う必要はありません。 しかし、これには欠点があります。 Expressionsを使用する際に非常に困難なタイピングがなければ、作成したコードが正しく機能するかどうかはわかりません。 したがって、Roslynを使用すると、Expressionsで記述されたコードよりもエラーが発生する可能性が高くなります。

それでも、Roslynの利点は素晴らしいです。 言語全体をカバーしています。つまり、任意のデザインを作成できます。 たとえば、実行時に新しいタイプを作成する必要がある場合は、Roslynを参照できます。 実行時に存在しないオブジェクトのDTO(データ転送オブジェクト)を作成するとします。 AutoMapperは通常開発中に使用されるため、AutoMapperの助けを借りたくありません。 作成されたタイプは、それぞれ異なるタイプのイベントをフィルターできる必要があります。 Expressionを指定する場合は、Expressionを作成してから、このデータを表す型を操作する必要があります。 そして、それらの逆シリアル化には、DTOが必要です。







Roslynを使用してコードを生成する最初の最も簡単な方法は、APIを備えたパーサーです。















テキストを分析し、構文ツリーを作成して、さまざまな操作を実行できます。形式の変更、美しいインデントの作成、変換などです。 APIのリファクタリング、変数の名前の変更、または呼び出し( Console.WriteLine



Console.Write



など)に置き換える必要があるとします。 すべてをゼロから作成する代わりに、既存のコードを読み取ってテンプレートとして使用し、必要なものだけを置き換えることができます。 訪問者テンプレートは、この目的に非常に適しています。 アプリケーション内のいくつかのトークンにアクセスして、正しいトークンを見つけたら、それを置き換えます。 スライドからわかるように、書式設定は非常に簡単です。







この機能が十分でない場合は、SyntaxGeneratorを使用できます。 これは強力な高レベルAPIであり、その下に構文ファクトリがあります。 名前空間、クラス、属性、パラメーターを宣言できます。つまり、本格的な言語です。 また、 node.AdjustWhitespace()



コマンドを使用すると、ノード間に標準のスペースを作成できます。















最初に、このツールがどのように機能するかのいくつかの例を見てみましょう。 最初のものでは、 SyntaxFactory



を使用し、そこからSyntaxTrivia



QualifiedName



CompilationUnit



UsingDirective



ます。 これは式ツリーよりもさらに悪いと言うかもしれません。 ただし、ここに表示されるのは低レベルAPIです。 知っておくと便利であり、Roslyn SDKを使用して調べることができます。 ここでは、コードの構文ツリーがどのように作成されるか、Roslynのノードがどのように互いに結合するかを確認できます。 , , , , , -, , . , , .







, . . , , , , . , . -, . , . -, . - . .







Roslyn .























text



:















, ( text2



):















, StringBuilder



- .







.















:















PostProcess(SyntaxNode root)



. , LINQ , , . , Console.WriteLine



Console.Write



. Console.ReadKey()



. Console.Write



Console.ReadKey



.















, . .







.















, CodeGenerationHelper()



. SyntaxGenerator , , .















POCO DTO, .







, .















, ? , , , . . . , — . .















, , , , , DTO. , . , . , .







AddImplementINotifyPropertyChanged()



.























, result.DiagnosticReport



, INotifyPropertyChanged



.















, string _name



OnPropertyChanged()



, OnPropertyChanged



, [CallerMemberName]



— . . , . . GitHub, .







— ? , — , SyntaxGenerator. , , , . , . SimpleClassGenerator



.















, HashSet<PortableExecutableReference> Reference



, System.Runtime



. , .NET Core, .NET Framework, — .NET Core, .















SimpleClassGenerator



, IDictionary<string, Properties> Properties



, . GetSource()



, BuildClass()



, .















-, CreateProperty()



.















, , . , , . . backfield. . Accessor



.







. , IL? , , , Reflection.Emit



, . , . . x86-. , , . . - . , , « ». - , .







IL , , . , .dll, , . ILSpy . : , , . , . - , Visual Studio IL, . «F11», , .







, . , , . Mono.Cecil — . , , , , , . , . , GitHub , . , . , «F11» , IL, .







? sample1.dll



, DataHelper



, .















Employee



DTO. Person



, Printer



, . , , Main



, .















Start1



Person



. Start2



, -, , , , Printer



. , for-each ToList()



. Linq , `Enumerable .







, . AssemblyHooker



, (trap) .dll. VisualStudio Code, . , sample1.dll.















. , . , PlantUML ? , , , .















, . , . , . , , , WriteLine



, . : Program



, Person



, Console



. , .















Main



sample1.dll. . , . , , PlantUML . , Enumerable



, . . , . , , . Main



.







, , , IL . , -, , . IL .







, , , . , . : , , , . . ? . , , .







まとめると。 , . , — Roslyn Quoter . , , Roslyn, . , Roslyn Quoter , .







広告の分。 おそらくご存知のように、会議を行っています。 今後の .NET会議はDotNext 2018 Piterです。 2018年4月22〜23日、サンクトペテルブルクで開催されます。 どんなレポートがありますか-YouTubeのアーカイブで見ることができます。 会議では、各レポートの後、特別なディスカッションゾーンで講演者や最高の.NETエキスパートとライブチャットを行うことができます。 要するに、私たちはあなたを待っています。



All Articles