ライブラリ間であまりにも多くの型情報を共有することで、明確なコンポーネント化の目標が達成できなくなっていませんか? おそらく、効率的な厳密に型指定されたデータ ストレージが必要ですが、オブジェクト モデルが進化するたびにデータベース スキーマを更新する必要がある場合、コストが非常に高くなります。
むしろ、実行時に型スキーマを推測する必要がありますか? 任意のユーザー オブジェクトを受け入れ
、それらを何らかのインテリジェントな方法で処理するコンポーネントを提供する必要がありますか?
実行時の柔軟性を最大限に高めながら、厳密に型指定されたデータ構造を維持するには、おそらくリフレクションと、それがソフトウェアをどのように改善できるかを検討する必要があるでしょう。このコラムでは、Microsoft .NET Framework の System.Reflection 名前空間と、それが開発エクスペリエンスにどのようなメリットをもたらすかについて説明します。いくつかの簡単な例から始めて、実際のシリアル化状況を処理する方法で終わります。その過程で、リフレクションと CodeDom がどのように連携して実行時データを効率的に処理するかを示します。
System.Reflection について詳しく説明する前に、リフレクティブ プログラミング全般について説明したいと思います。まず、リフレクションは、プログラマがコード エンティティのアイデンティティや形式的構造を事前に知らなくても検査および操作できるようにする、プログラミング システムによって提供される機能として定義できます。このセクションで説明する内容はたくさんあるので、一つずつ説明していきます。
まず、リフレクションは何を提供するのでしょうか? それを使って何ができるのでしょうか? 私は、一般的なリフレクション中心のタスクを検査と操作の 2 つのカテゴリに分類する傾向があります。検査では、オブジェクトと型を分析して、その定義と動作に関する構造化された情報を収集する必要があります。いくつかの基本的な規定を除けば、これは多くの場合、それらについての事前知識なしに行われます。 (たとえば、.NET Framework では、すべてが System.Object から継承され、多くの場合、オブジェクト型への参照がリフレクションの一般的な開始点となります。)
操作では、検査、新しいインスタンスの作成、またはさらには収集を通じて収集された情報を使用してコードを動的に呼び出します。型とオブジェクトは簡単に動的に再構築できます。重要な点の 1 つは、ほとんどのシステムでは、実行時に型とオブジェクトを操作すると、ソース コードで静的に同等の操作を実行する場合と比べてパフォーマンスが低下するということです。これは、リフレクションの動的な性質により必要なトレードオフですが、リフレクションのパフォーマンスを最適化するためのヒントやベスト プラクティスが数多くあります (最適化の詳細については、msdn.microsoft.com/msdnmag/issues/05 を参照してください)。反射の使用/07/反射)。
では、リフレクションの目的は何でしょうか? プログラマは実際に何を検査し、操作するのでしょうか? リフレクションの定義では、プログラマの観点からは、リフレクション技術が境界線を曖昧にする場合があるという事実を強調するために、「コード エンティティ」という新しい用語を使用しました。伝統的なオブジェクトとタイプ。たとえば、典型的なリフレクション中心のタスクは次のようになります。
オブジェクト O へのハンドルから開始し、リフレクションを使用してそれに関連付けられた定義 (タイプ T) へのハンドルを取得します。
型 T を調べて、そのメソッド M へのハンドルを取得します。
別のオブジェクト O' (これも型 T) のメソッド M を呼び出します。
あるインスタンスからその基になる型、その型からメソッドに移動し、そのメソッドのハンドルを使用して別のインスタンスでそれを呼び出していることに注意してください。明らかに、これはソース コードで従来の C# プログラミングを使用しています。テクノロジーではこれを実現できません。以下で .NET Framework の System.Reflection について説明した後、この状況を具体的な例で再度説明します。
一部のプログラミング言語は構文を通じてネイティブにリフレクションを提供しますが、他のプラットフォームやフレームワーク (.NET Framework など) はシステム ライブラリとしてリフレクションを提供します。リフレクションがどのように提供されるかに関係なく、特定の状況でリフレクション テクノロジを使用する可能性は非常に複雑です。プログラミング システムがリフレクションを提供できるかどうかは、次のような多くの要因によって決まります。プログラマーは、プログラミング言語の機能をうまく利用して概念を表現していますか? コンパイラーは、将来の分析を容易にするために十分な構造化情報 (メタデータ) を出力に埋め込んでいますか?
解釈 このメタデータをダイジェストするランタイム サブシステムまたはホスト インタープリターはありますか?複雑なオブジェクト指向の型システムを念頭に置いて、
プラットフォーム ライブラリはこの解釈の結果を提供しますか?
コード内では単純な C スタイルの関数として表示され、正式なデータ構造が存在しない場合、特定の変数 v1 のポインタが特定の型 T のオブジェクト インスタンスを指していることをプログラムが動的に推論することは明らかに不可能です。 。結局のところ、タイプ T は頭の中の概念であり、プログラミング文に明示的に現れることはありません。しかし、より柔軟なオブジェクト指向言語 (C# など) を使用してプログラムの抽象構造を表現し、型 T の概念を直接導入すると、コンパイラーはそのアイデアを後で API に渡すことができるものに変換します。共通言語ランタイム (CLR) または動的言語インタープリターによって提供される、フォームを理解するための適切なロジック。
リフレクションは完全に動的なランタイム テクノロジーなのでしょうか? 簡単に言うと、そうではありません。開発および実行サイクル全体を通じて、リフレクションが利用可能で開発者にとって役立つ時期が何度もあります。一部のプログラミング言語は、高レベルのコードをマシンが理解できる命令に直接変換するスタンドアロン コンパイラーを通じて実装されます。出力ファイルにはコンパイルされた入力のみが含まれており、ランタイムには不透明なオブジェクトを受け入れてその定義を動的に分析するためのサポート ロジックがありません。これは、多くの従来の C コンパイラに当てはまります。ターゲットの実行可能ファイルにはサポートするロジックがほとんどないため、動的リフレクションをあまり行うことはできませんが、コンパイラは静的リフレクションを時々提供します。たとえば、ユビキタスな typeof 演算子を使用すると、プログラマはコンパイル時に型識別子をチェックできます。
まったく異なる状況は、解釈されたプログラミング言語が常にメイン プロセスを通じて実行されることです (スクリプト言語は通常、このカテゴリに分類されます)。プログラムの完全な定義が (入力ソース コードとして) 利用可能であり、完全な言語実装 (インタープリター自体として) と組み合わせることで、自己分析をサポートするために必要なすべてのテクニックが整います。この動的言語は、プログラムの動的分析と操作のための豊富なツール セットに加えて、包括的なリフレクション機能を提供することがよくあります。
.NET Framework CLR とそのホスト言語 (C# など) はその中間にあります。コンパイラは、ソース コードを IL およびメタデータに変換するために使用されます。後者は、ソース コードよりも低レベル、つまり「論理的」ではありませんが、依然として多くの抽象的な構造および型情報を保持します。 CLR が起動してこのプログラムをホストすると、基本クラス ライブラリ (BCL) の System.Reflection ライブラリはこの情報を使用して、オブジェクト型、型メンバー、メンバー署名などに関する情報を返すことができます。さらに、遅延バインディング呼び出しを含む呼び出しもサポートできます。
.NET でのリフレクション
.NET Framework でプログラミングするときにリフレクションを利用するには、System.Reflection 名前空間を使用できます。この名前空間は、アセンブリ、モジュール、型、メソッド、コンストラクター、フィールド、プロパティなどの多くのランタイム概念をカプセル化するクラスを提供します。図 1 の表は、System.Reflection のクラスが概念的なランタイムのクラスにどのようにマップされるかを示しています。
System.Reflection.Assembly と System.Reflection.Module は重要ですが、主に新しいコードを見つけてランタイムに読み込むために使用されます。このコラムでは、これらの部分については説明しません。関連するコードはすべてすでにロードされているものと仮定します。
ロードされたコードを検査して操作する場合、一般的なパターンは主に System.Type です。通常は、対象のランタイム クラスの System.Type インスタンスを (Object.GetType 経由で) 取得することから始めます。その後、System.Type のさまざまなメソッドを使用して、System.Reflection 内の型の定義を調べ、他のクラスのインスタンスを取得できます。たとえば、特定のメソッドに興味があり、このメソッドの System.Reflection.MethodInfo インスタンスを (おそらく Type.GetMethod を通じて) 取得したい場合です。同様に、フィールドに興味があり、このフィールドの System.Reflection.FieldInfo インスタンスを (おそらく Type.GetField を通じて) 取得したい場合も同様です。
必要な反射インスタンス オブジェクトをすべて取得したら、必要に応じて検査または操作の手順に従って続行できます。チェックするときは、リフレクティブ クラスのさまざまな説明プロパティを使用して、必要な情報を取得します (これはジェネリック型ですか? これはインスタンス メソッドですか?)。操作時には、メソッドを動的に呼び出して実行したり、コンストラクターを呼び出して新しいオブジェクトを作成したりすることができます。
型とメンバーのチェック
コードに移り、基本的なリフレクションを使用してチェックする方法を検討してみましょう。型分析に焦点を当てます。オブジェクトから始めて、その型を取得し、次にいくつかの興味深いメンバーを調べます (図 2 を参照)。
まず最初に注意すべきことは、クラス定義では、メソッドを記述するためのスペースが予想よりもはるかに多いように見えることです。これらの追加のメソッドはどこから来たのでしょうか? .NET Framework のオブジェクト階層に精通している人なら、これらのメソッドが共通の基本クラス Object 自体から継承されたものであることがわかります。 (実際、最初に Object.GetType を使用してその型を取得しました。) さらに、プロパティのゲッター関数も確認できます。では、明示的に定義された MyClass 自体の関数だけが必要な場合はどうすればよいでしょうか? 言い換えれば、継承された関数だけを非表示にするにはどうすればよいでしょうか? あるいは
、MSDN をオンラインで調べればわかるでしょう
。BindingFlags パラメーターを受け入れる GetMethods の 2 番目のオーバーロード メソッドを誰もが喜んで使用することがわかりました。 BindingFlags 列挙体のさまざまな値を組み合わせることで、関数がメソッドの必要なサブセットのみを返すようにすることができます。 GetMethods 呼び出しを次のように置き換えます:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |BindingFlags.Public)
その結果、次の出力が得られます (静的ヘルパー関数や System.Object から継承された関数がないことに注意してください)。
リフレクション デモの例 1
型名: MyClass
メソッド名: MyMethod1
メソッド名: MyMethod2
メソッド名: get_MyProperty
プロパティ名: MyProperty
型名 (完全修飾) とメンバーが事前にわかっている場合はどうすればよいでしょうか。最初の 2 つの例のコードでは、プリミティブ クラス ブラウザを実装するための基本コンポーネントがすでに用意されています。ランタイム エンティティを名前で検索し、そのさまざまな関連プロパティを列挙できます。
コードを動的に呼び出す
これまで、ランタイム オブジェクト (型やメソッドなど) へのハンドルを、名前の出力などの説明目的のみで取得してきました。しかし、実際にメソッドを呼び出すにはどうすればよいでしょうか?
この例のいくつかの重要な点は次のとおりです。まず、MyClass のインスタンス mc1 から MethodInfo インスタンスを取得します。そのタイプ。最後に、MethodInfo が呼び出されるとき、呼び出しの最初のパラメータとして渡すことによって、別の MyClass (mc2) インスタンスにバインドされます。
前述したように、この例では、ソース コードで期待される型とオブジェクトの使用法の区別があいまいになります。論理的には、メソッドのハンドルを取得し、そのメソッドを別のオブジェクトに属しているかのように呼び出します。関数型プログラミング言語に精通しているプログラマーにとっては、これは簡単かもしれませんが、C# にしか精通していないプログラマーにとっては、オブジェクトの実装とオブジェクトのインスタンス化を分離するのはそれほど直感的ではないかもしれません。
すべてをまとめる
ここまで、チェックとコールの基本原則について説明しましたが、具体的な例とともにまとめていきます。オブジェクトを処理する必要がある静的ヘルパー関数を備えたライブラリを提供したいと想像してください。しかし、設計時には、これらのオブジェクトの型についてまったくわかりません。それは、これらのオブジェクトから意味のある情報をどのように抽出するかについての関数呼び出し者の指示によって異なります。この関数は、オブジェクトのコレクションとメソッドの文字列記述子を受け入れます。次に、コレクションを反復処理し、各オブジェクトのメソッドを呼び出し、何らかの関数で戻り値を集計します。
この例では、いくつかの制約を宣言します。まず、文字列パラメーターで記述されたメソッド (各オブジェクトの基になる型で実装する必要がある) はパラメーターを受け入れず、整数を返します。コードは、指定されたメソッドを呼び出してオブジェクトのコレクションを反復処理し、すべての値の平均を徐々に計算します。最後に、これは製品コードではないため、パラメータの検証や合計時の整数のオーバーフローを心配する必要はありません。
サンプル コードを参照すると、メイン関数と静的ヘルパー ComputeAverage の間の合意が、オブジェクト自体の共通基本クラス以外の型情報に依存していないことがわかります。言い換えれば、転送されるオブジェクトの型と構造を完全に変更できますが、常に文字列を使用して整数を返すメソッドを記述できる限り、ComputeAverage は問題なく動作する
という点に注意してください
。最後の例は MethodInfo (一般的なリフレクション) に関連しています。 ComputeAverage の foreach ループでは、コードはコレクション内の最初のオブジェクトから MethodInfo のみを取得し、それを後続のすべてのオブジェクトの呼び出しにバインドすることに注意してください。コーディングが示すように、これは正常に動作します。これは MethodInfo キャッシュの簡単な例です。しかし、ここには根本的な制限があります。 MethodInfo インスタンスは、取得するオブジェクトと同じ階層型のインスタンスによってのみ呼び出すことができます。これは、IntReturner と SonOfIntReturner のインスタンス (IntReturner から継承) が渡されるため可能です。
サンプル コードには、EnemyOfIntReturner という名前のクラスが含まれています。このクラスは、他の 2 つのクラスと同じ基本プロトコルを実装しますが、共通の共有型を共有しません。言い換えれば、インターフェイスは論理的には同等ですが、タイプ レベルでの重複はありません。この状況での MethodInfo の使用法を調べるには、コレクションに別のオブジェクトを追加し、「new EnemyOfIntReturner(10)」によってインスタンスを取得し、サンプルを再度実行してみてください。 MethodInfo は、(メソッド名と基になるプロトコルが同等であっても) MethodInfo の取得元の元の型とは全く関係がないため、指定されたオブジェクトの呼び出しに MethodInfo を使用できないことを示す例外が発生します。コードを本番環境に対応させるには、この状況に遭遇する準備ができている必要があります。
考えられる解決策は、すべての受信オブジェクトの型を自分で分析し、共有型階層 (存在する場合) の解釈を保持することです。次のオブジェクトの型が既知の型階層と異なる場合は、新しい MethodInfo を取得して保存する必要があります。もう 1 つの解決策は、TargetException をキャッチして MethodInfo インスタンスを再取得することです。ここで説明した両方のソリューションには長所と短所があります。 Joel Pobar は、この雑誌の 2007 年 5 月号に、MethodInfo のバッファリングとリフレクションのパフォーマンスに関する優れた記事を書いています。これを強くお勧めします。
この例では、アプリケーションまたはフレームワークにリフレクションを追加して、将来のカスタマイズや拡張性をさらに柔軟にする方法を示しています。確かに、リフレクションの使用は、ネイティブ プログラミング言語の同等のロジックに比べて少し面倒になる可能性があります。リフレクションベースの遅延バインディングをコードに追加するのがあなたやクライアントにとって面倒すぎると感じる場合 (結局のところ、クライアントはフレームワーク内で型とコードを何らかの形で考慮する必要があるため)、それは適度な柔軟性でのみ必要になる可能性があります。ある程度のバランスを達成するために。
シリアル化のための効率的な型処理
いくつかの例を通して .NET リフレクションの基本原理を説明したので、実際の状況を見てみましょう。ソフトウェアが Web サービスまたはその他のプロセス外リモート処理テクノロジを通じて他のシステムと対話する場合、シリアル化の問題が発生する可能性があります。シリアル化は基本的に、メモリを占有するアクティブなオブジェクトを、オンライン送信またはディスク ストレージに適したデータ形式に変換します。
.NET Framework の System.Xml.Serialization 名前空間は、XmlSerializer を備えた強力なシリアル化エンジンを提供します。これは、任意のマネージド オブジェクトを取得して XML に変換できます (将来的には、XML データを型付きオブジェクト インスタンスに変換し直すこともできます)。これはデシリアライゼーションと呼ばれます)。 XmlSerializer クラスは、プロジェクトでシリアル化の問題が発生した場合に最初に選択する強力なエンタープライズ対応ソフトウェアです。ただし、教育目的のために、シリアル化 (または他の同様のランタイム型処理インスタンス) を実装する方法を検討してみましょう。
これを考慮してください。あなたは、任意のユーザー タイプのオブジェクト インスタンスを取得し、それらをスマート データ形式に変換するフレームワークを提供していると考えます。たとえば、以下に示すように、タイプ Address のメモリ常駐オブジェクトがあると仮定します。
(擬似コード)
クラスアドレス
{
AddressID ID;
ストリングストリート、シティ;
StateType 状態。
ZipCodeType 郵便番号;
}
後で使用するために適切なデータ表現を生成するにはどうすればよいでしょうか? おそらく、単純なテキスト レンダリングでこの問題は解決されるでしょう:
住所: 123
Street: 1 Microsoft Way
City: Redmond
State: WA
Zip: 98052
変換する必要がある正式なデータが完全に理解されている場合事前に次のように入力しておくと (たとえば、自分でコードを書くとき)、物事は非常に簡単になります:
foreach(Address a in AddressList)
{
Console.WriteLine(“アドレス:{0}”, a.ID);
Console.WriteLine(“tStreet:{0}”, a.Street);
... // 等々
}
ただし、実行時にどのようなデータ型が発生するか事前にわかっていないと、非常に興味深い事態になる可能性があります。このような一般的なフレームワーク コードはどのように記述しますか?
MyFramework.TranslateObject(object input, MyOutputWriter Output)
まず、シリアル化に役立つ型メンバーを決定する必要があります。可能性としては、プリミティブ システム型などの特定の型のメンバーのみをキャプチャすることや、型メンバーのマーカーとしてカスタム プロパティを使用するなど、シリアル化する必要があるメンバーを型作成者に示すメカニズムを提供することが含まれます。プリミティブ システム型などの特定の型のメンバーのみをキャプチャすることも、型の作成者がシリアル化する必要があるメンバーを指定することもできます (カスタム プロパティを型メンバーのマーカーとして使用する可能性があります)。
変換する必要があるデータ構造メンバーを文書化したら、次に行う必要があるのは、それらを列挙し、受信オブジェクトから取得するロジックを作成することです。ここでの面倒な作業はリフレクションによって行われ、データ構造とデータ値の両方をクエリできるようになります。
わかりやすくするために、オブジェクトを受け取り、そのすべてのパブリック プロパティ値を取得し、ToString を直接呼び出してそれらを文字列に変換し、値をシリアル化する軽量の変換エンジンを設計してみましょう。 「input」という名前の特定のオブジェクトの場合、アルゴリズムは大まかに次のとおりです。
input.GetType を呼び出して、入力の基礎となる構造を記述する System.Type インスタンスを取得します。
Type.GetProperties と適切な BindingFlags パラメーターを使用して、パブリック プロパティを PropertyInfo インスタンスとして取得します。
プロパティは、PropertyInfo.Name と PropertyInfo.GetValue を使用してキーと値のペアとして取得されます。
各値に対して Object.ToString を呼び出して、(基本的な方法で) 文字列形式に変換します。
オブジェクト型の名前、プロパティ名と文字列値のコレクションを正しいシリアル化形式にパックします。
このアルゴリズムは、実行時のデータ構造を取得して自己記述型データに変換するという点を捉えながら、作業を大幅に簡素化します。しかし、パフォーマンスという問題があります。前に述べたように、リフレクションは型の処理と値の取得の両方に非常にコストがかかります。この例では、提供された型の各インスタンスに対して完全な型分析を実行します。
型の構造を何らかの方法で取得または保持して、後で簡単に取得して、その型の新しいインスタンスを効率的に処理できる、つまり、アルゴリズム例のステップ 3 に進むことができたらどうなるでしょうか。ニュースは、.NET Framework の機能を使用してこれを行うことができるということです。型のデータ構造を理解したら、CodeDom を使用して、そのデータ構造にバインドするコードを動的に生成できます。ヘルパー クラスと、受信型を参照してそのプロパティ (マネージ コード内の他のプロパティと同様) に直接アクセスするメソッドを含むヘルパー アセンブリを生成できるため、型チェックがパフォーマンスに影響を与えるのは 1 回だけです。
次に、このアルゴリズムを修正します。新しい型:
この型に対応する System.Type インスタンスを取得します。
さまざまな System.Type アクセサーを使用して、プロパティ名、フィールド名などのスキーマ (または少なくともシリアル化に役立つスキーマのサブセット) を取得します。
スキーマ情報を使用して、新しい型とリンクし、インスタンスを効率的に処理するヘルパー アセンブリを (CodeDom 経由で) 生成します。
ヘルパー アセンブリ内のコードを使用して、インスタンス データを抽出します。
必要に応じてデータをシリアル化します。
特定のタイプのすべての受信データについて、ステップ 4 に進むと、各インスタンスを明示的にチェックするよりもパフォーマンスが大幅に向上します。
私は、リフレクションと CodeDom (このコラムでダウンロード可能) を使用してこのアルゴリズムを実装する SimpleSerialization と呼ばれる基本的なシリアル化ライブラリを開発しました。主要なコンポーネントは SimpleSerializer という名前のクラスで、ユーザーが System.Type のインスタンスを使用して構築します。コンストラクターでは、新しい SimpleSerializer インスタンスが指定された型を分析し、ヘルパー クラスを使用して一時アセンブリを生成します。ヘルパー クラスは、指定されたデータ型に緊密にバインドされており、型に関する完全な事前知識を持ってコードを作成しているかのようにインスタンスを処理します。
SimpleSerializerクラス
のレイアウトは次のとおりです。
{
パブリッククラス SimpleSerializer(Type dataType);
public void Serialize(オブジェクト入力、SimpleDataWriterライター);
}
単純に驚くべきことです。コンストラクターは面倒な作業を行います。リフレクションを使用して型構造を分析し、CodeDom を使用してヘルパー アセンブリを生成します。 SimpleDataWriter クラスは、一般的なシリアル化パターンを示すために使用される単なるデータ シンクです。
を
シリアル化するには
、次の疑似
コードを
使用してタスクを完了します
。
サンプル コード、特に SimpleSerialization ライブラリを自分で試してみることをお勧めします。 SimpleSerializer のいくつかの興味深い部分にコメントを追加しました。お役に立てれば幸いです。もちろん、運用コードで厳密なシリアル化が必要な場合は、.NET Framework で提供されるテクノロジ (XmlSerializer など) に依存する必要があります。ただし、実行時に任意の型を操作して効率的に処理する必要がある場合は、ソリューションとして私の SimpleSerialization ライブラリを採用していただければ幸いです。