Typeパラメータがコンパイル時にはわからないが、実行時に動的に取得されるときにジェネリックメソッドを呼び出すための最良の方法は何ですか?
次のサンプルコードを考えてみましょう - Example()
メソッド内で、Type
変数に格納されているmyType
を使用してGenericMethod<T>()
を呼び出す最も簡潔な方法は何ですか?
public class Sample
{
public void Example(string typeName)
{
Type myType = FindType(typeName);
// What goes here to call GenericMethod<T>()?
GenericMethod<myType>(); // This doesn't work
// What changes to call StaticMethod<T>()?
Sample.StaticMethod<myType>(); // This also doesn't work
}
public void GenericMethod<T>()
{
// ...
}
public static void StaticMethod<T>()
{
//...
}
}
リフレクションを使用してメソッドを開始する必要があります。次に、 MakeGenericMethod を使用して型引数を指定してメソッドを「構築」します。
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
静的メソッドの場合は、null
の最初の引数としてInvoke
を渡します。それは一般的な方法とは関係ない - それは単なる通常の反射です。
すでに述べたように、これの多くはdynamic
を使用したC#4以降のものです - もちろん型推論を使用できるのであれば。質問の正確な例のように、型推論が利用できない場合には役に立ちません。
元の答えに追加するだけです。これは動作しますが:
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
GenericMethod
のコンパイル時チェックを失うという点でも少し危険です。後でリファクタリングを行ってGenericMethod
に名前を変更しても、このコードは気付かれず、実行時に失敗します。また、アセンブリの後処理(未使用のメソッドやクラスの難読化や削除など)があると、このコードも壊れる可能性があります。
ですから、コンパイル時にリンクしているメソッドがわかっていて、これが何百万回も呼び出されていないのでオーバーヘッドが問題にならない場合は、このコードを次のように変更します。
Action<> GenMethod = GenericMethod<int>; //change int by any base type
//accepted by GenericMethod
MethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
あまり綺麗ではありませんが、ここでGenericMethod
をコンパイル時に参照しています。GenericMethod
でリファクタリング、削除、または何かをすると、このコードは機能し続けるか、少なくともコンパイル時に中断します(例えばGenericMethod
を削除する場合)。
同じことをする他の方法は、新しいラッパークラスを作成し、それをActivator
を通して作成することです。もっと良い方法があるかどうかわかりません。
リフレクションAPIの代わりに dynamic
typeを使用すると、実行時にのみ認識される型パラメータを使用して汎用メソッドを呼び出すことが非常に簡単になります。
このテクニックを使うためには、型は実際のオブジェクト(Type
クラスのインスタンスだけではなく)から知られていなければなりません。それ以外の場合は、そのタイプのオブジェクトを作成するか、標準のリフレクションAPI solution を使用する必要があります。 Activator.CreateInstance メソッドを使用してオブジェクトを作成できます。
一般的なメソッドを呼び出したい場合、「通常の」使用法ではその型が推測されているはずですが、それは単に未知の型のオブジェクトをdynamic
にキャストすることになります。これが例です:
class Alpha { }
class Beta { }
class Service
{
public void Process<T>(T item)
{
Console.WriteLine("item.GetType(): " + item.GetType()
+ "\ttypeof(T): " + typeof(T));
}
}
class Program
{
static void Main(string[] args)
{
var a = new Alpha();
var b = new Beta();
var service = new Service();
service.Process(a); // Same as "service.Process<Alpha>(a)"
service.Process(b); // Same as "service.Process<Beta>(b)"
var objects = new object[] { a, b };
foreach (var o in objects)
{
service.Process(o); // Same as "service.Process<object>(o)"
}
foreach (var o in objects)
{
dynamic dynObj = o;
service.Process(dynObj); // Or write "service.Process((dynamic)o)"
}
}
}
そして、これはこのプログラムの出力です:
item.GetType(): Alpha typeof(T): Alpha
item.GetType(): Beta typeof(T): Beta
item.GetType(): Alpha typeof(T): System.Object
item.GetType(): Beta typeof(T): System.Object
item.GetType(): Alpha typeof(T): Alpha
item.GetType(): Beta typeof(T): Beta
Process
は、(GetType()
メソッドを使用して)渡された引数の実際の型と(typeof
演算子を使用して)汎用パラメータの型を書き込む汎用インスタンスメソッドです。
Object引数をdynamic
型にキャストすることで、実行時までtypeパラメータの提供を延期しました。 Process
メソッドがdynamic
引数を指定して呼び出されると、コンパイラーはこの引数の型を気にしません。コンパイラは、実行時に(リフレクションを使用して)渡された引数の実際の型をチェックし、呼び出すための最良のメソッドを選択するコードを生成します。ここには唯一の一般的なメソッドがあるので、それは適切な型パラメータで呼び出されます。
この例では、出力はあなたが書いたものと同じです。
foreach (var o in objects)
{
MethodInfo method = typeof(Service).GetMethod("Process");
MethodInfo generic = method.MakeGenericMethod(o.GetType());
generic.Invoke(service, new object[] { o });
}
動的型のバージョンは間違いなく短く、書きやすいです。また、この関数を複数回呼び出すことによるパフォーマンスについても心配しないでください。同じタイプの引数を使用した次の呼び出しは、DLRの caching メカニズムのおかげで速くなります。もちろん、呼び出されたデリゲートをキャッシュするコードを書くこともできますが、dynamic
型を使うことでこの動作を無料で手に入れることができます。
呼び出したいジェネリックメソッドが、パラメータ化された型の引数を持たない(その型パラメータが推測できない)場合は、次の例のように、ジェネリックメソッドの呼び出しをヘルパーメソッドでラップできます。
class Program
{
static void Main(string[] args)
{
object obj = new Alpha();
Helper((dynamic)obj);
}
public static void Helper<T>(T obj)
{
GenericMethod<T>();
}
public static void GenericMethod<T>()
{
Console.WriteLine("GenericMethod<" + typeof(T) + ">");
}
}
リフレクションAPIを使用する代わりにdynamic
オブジェクトを使用することで本当に素晴らしいのは、実行時までわからないこの特定の型のコンパイル時チェックだけを失うことです。他の引数とメソッドの名前は、通常どおりコンパイラによって静的に分析されます。引数を削除または追加したり、それらの型を変更したり、メソッド名を変更したりすると、コンパイル時エラーが発生します。メソッド名をType.GetMethod
に文字列として、引数をMethodInfo.Invoke
にobjects配列として指定しても、これは起こりません。
以下は、コンパイル時(コメント付きのコード)および実行時に他のエラーを検出する方法を示す簡単な例です。また、DLRがどのメソッドを呼び出すかを解決する方法も示しています。
interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }
class Program
{
static void Main(string[] args)
{
var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
for (int i = 0; i < objects.Length; i++)
{
ProcessItem((dynamic)objects[i], "test" + i, i);
//ProcesItm((dynamic)objects[i], "test" + i, i);
//compiler error: The name 'ProcesItm' does not
//exist in the current context
//ProcessItem((dynamic)objects[i], "test" + i);
//error: No overload for method 'ProcessItem' takes 2 arguments
}
}
static string ProcessItem<T>(T item, string text, int number)
where T : IItem
{
Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
typeof(T), text, number);
return "OK";
}
static void ProcessItem(BarItem item, string text, int number)
{
Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
}
}
ここでも、引数をdynamic
型にキャストすることによって何らかのメソッドを実行します。最初の引数の型の検証のみがランタイムに延期されます。呼び出しているメソッドの名前が存在しない場合、または他の引数が無効な場合(間違った数の引数または間違った型)、コンパイラエラーが発生します。
dynamic
引数をメソッドに渡すと、この呼び出しは Late-bound になります。メソッドオーバーロードの解決は実行時に行われ、最適なオーバーロードを選択しようとします。そのため、ProcessItem
型のオブジェクトを使ってBarItem
メソッドを呼び出すと、実際には非ジェネリックメソッドが呼び出されます。これは、この型により適しているためです。ただし、このオブジェクトを処理できるメソッドがないため、Alpha
型の引数を渡すとランタイムエラーが発生します(一般的なメソッドにはwhere T : IItem
という制約があり、Alpha
クラスにはこのインターフェイスが実装されていません)。しかしそれがすべてのポイントです。コンパイラはこの呼び出しが有効であるという情報を持っていません。プログラマーとしてのあなたはこれを知っています、そしてあなたはこのコードがエラーなしで実行されることを確認するべきです。
動的型のパラメータで非voidメソッドを呼び出す場合、その戻り型はおそらく be dynamic
も になります。したがって、前の例をこのコードに変更するとします。
var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
結果オブジェクトの型はdynamic
になります。これは、どのメソッドが呼び出されるのかをコンパイラが常に認識しているわけではないためです。関数呼び出しの戻り型がわかっている場合は、それを必要な型に 暗黙的に変換 して、残りのコードを静的に型指定する必要があります。
string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
型が一致しないとランタイムエラーが発生します。
実際に、前の例の結果値を取得しようとすると、2回目のループ反復でランタイムエラーが発生します。これは、void関数の戻り値を保存しようとしたためです。
C#4.0では、DLRがランタイム型を使用してそれを呼び出すことができるため、リフレクションは必要ありません。 DLRライブラリを使用するのは(C#コンパイラがコードを生成するのではなく)動的にちょっと面倒なので、オープンソースフレームワーク Dynamitey (.net standard 1.5)を使用すると、同じ呼び出しに簡単にランタイムアクセスできます。コンパイラが生成します。
var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));
var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));
型情報から汎用メソッドを呼び出すには、3つのステップがあります。
((Action)GenericMethod<object>)
.Method
.GetGenericMethodDefinition()
.MakeGenericMethod(typeof(string))
.Invoke(this, null);
GenericMethod<object>
は呼び出すメソッドの名前で、一般的な制約を満たす任意の型です。
(アクション)呼び出されるメソッドのシグネチャと一致する、すなわち(Func<string,string,int>
またはAction<bool>
)
MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
メソッドを含むクラスの内側から:
MethodInfo method = ((Action)GenericMethod<object>)
.Method
.GetGenericMethodDefinition();
MethodInfo method = ((Action)StaticMethod<object>)
.Method
.GetGenericMethodDefinition();
メソッドを含むクラスの外側から:
MethodInfo method = ((Action)(new Sample())
.GenericMethod<object>)
.Method
.GetGenericMethodDefinition();
MethodInfo method = ((Action)Sample.StaticMethod<object>)
.Method
.GetGenericMethodDefinition();
C#では、メソッドの名前、つまり "ToString"または "GenericMethod"は実際には1つ以上のメソッドを含むことができるメソッドのグループを指します。メソッドパラメータの種類を指定するまでは、どのメソッドを参照しているかはわかりません。
((Action)GenericMethod<object>)
は特定のメソッドのデリゲートを参照します。 ((Func<string, int>)GenericMethod<object>)
は、GenericMethodの異なるオーバーロードを参照しています
MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
(Sample v) => v.GenericMethod<object>()
)).Body).Method.GetGenericMethodDefinition();
これは
本体が目的のメソッドへの呼び出しであるラムダ式を作成します。
Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();
本体を抽出してMethodCallExpressionにキャストします。
MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;
メソッドから一般的なメソッド定義を取得する
MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);
誰も " 古典的なReflection "の解決策を提供していないので、完全なコード例は次のとおりです。
using System;
using System.Collections;
using System.Collections.Generic;
namespace DictionaryRuntime
{
public class DynamicDictionaryFactory
{
/// <summary>
/// Factory to create dynamically a generic Dictionary.
/// </summary>
public IDictionary CreateDynamicGenericInstance(Type keyType, Type valueType)
{
//Creating the Dictionary.
Type typeDict = typeof(Dictionary<,>);
//Creating KeyValue Type for Dictionary.
Type[] typeArgs = { keyType, valueType };
//Passing the Type and create Dictionary Type.
Type genericType = typeDict.MakeGenericType(typeArgs);
//Creating Instance for Dictionary<K,T>.
IDictionary d = Activator.CreateInstance(genericType) as IDictionary;
return d;
}
}
}
上記のDynamicDictionaryFactory
クラスはメソッドを持っています
CreateDynamicGenericInstance(Type keyType, Type valueType)
そしてそれはIDictionaryインスタンスを作成して返します。そのインスタンスのキーと値の型は呼び出しkeyType
とvalueType
で正確に指定されています。
これは完全な例です Dictionary<String, int>
をインスタンス化して使用するためにこのメソッドを呼び出す方法:
using System;
using System.Collections.Generic;
namespace DynamicDictionary
{
class Test
{
static void Main(string[] args)
{
var factory = new DictionaryRuntime.DynamicDictionaryFactory();
var dict = factory.CreateDynamicGenericInstance(typeof(String), typeof(int));
var typedDict = dict as Dictionary<String, int>;
if (typedDict != null)
{
Console.WriteLine("Dictionary<String, int>");
typedDict.Add("One", 1);
typedDict.Add("Two", 2);
typedDict.Add("Three", 3);
foreach(var kvp in typedDict)
{
Console.WriteLine("\"" + kvp.Key + "\": " + kvp.Value);
}
}
else
Console.WriteLine("null");
}
}
}
上記のコンソールアプリケーションが実行されると、正しい期待される結果が得られます。
Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3
これは、 Graxの答え に基づく私の2セントですが、ジェネリックメソッドには2つのパラメータが必要です。
メソッドがHelpersクラスで次のように定義されているとします。
public class Helpers
{
public static U ConvertCsvDataToCollection<U, T>(string csvData)
where U : ObservableCollection<T>
{
//transform code here
}
}
私の場合、U型は常にT型のオブジェクトを格納する観測可能なコレクションです。
私は自分の型を定義しているので、まず観察可能なコレクション(U)とその中に格納されているオブジェクト(T)を表す "ダミー"オブジェクトを作成します。
object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);
それからGetMethodを呼び出してあなたのGeneric関数を見つけます。
MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");
これまでのところ、上記の呼び出しは上で説明したものとほとんど同じですが、複数のパラメーターを渡す必要がある場合は少し異なります。
Type []配列をMakeGenericMethod関数に渡す必要があります。この関数には、上記で作成した「ダミー」オブジェクトの型が含まれています。
MethodInfo generic = method.MakeGenericMethod(
new Type[] {
myCollection.GetType(),
myObject.GetType()
});
それが終わったら、前述のようにInvokeメソッドを呼び出す必要があります。
generic.Invoke(null, new object[] { csvData });
そして、これで終わりです。魅力的に働く!
更新:
@Bevan氏が強調したように、MakeGenericMethod関数を呼び出すときにparamsを取り込むときに配列を作成する必要はなく、型を直接この関数に渡すことができるので、型を取得するためにオブジェクトを作成する必要もありません。私の場合は、別のクラスで型が事前定義されているので、コードを単に次のように変更しました。
object myCollection = null;
MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");
MethodInfo generic = method.MakeGenericMethod(
myClassInfo.CollectionType,
myClassInfo.ObjectType
);
myCollection = generic.Invoke(null, new object[] { csvData });
myClassInfoには、コンストラクタに渡されたenum値に基づいて実行時に設定したType
型の2つのプロパティが含まれています。これらの関連型をMakeGenericMethodで使用します。
この@Bevanをハイライトしてくれてありがとう。