Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
334 views
in Technique[技术] by (71.8m points)

c# - Select Right Generic Method with Reflection

I want to select the right generic method via reflection and then call it.

Usually this is quite easy. For example

var method = typeof(MyType).GetMethod("TheMethod");
var typedMethod = method.MakeGenericMethod(theTypeToInstantiate);

However the issue start when there are different generic overloads of the method. For example the static-methods in the System.Linq.Queryable-class. There are two definitions of the 'Where'-method

static IQueryable<T> Where(this IQueryable<T> source, Expression<Func<T,bool>> predicate)
static IQueryable<T> Where(this IQueryable<T> source, Expression<Func<T,int,bool>> predicate)

This meand that GetMethod doesn't work, because it cannot destiguish the two. Therefore I want to select the right one.

So far I often just took the first or second method, depending on my need. Like this:

var method = typeof (Queryable).GetMethods().First(m => m.Name == "Where");
var typedMethod = method.MakeGenericMethod(theTypeToInstantiate);

However I'm not happy with this, because I make a huge assumption that the first method is the right one. I rather want to find the right method by the argument type. But I couldn't figure out how.

I tried it with passing the 'types', but it didn't work.

        var method = typeof (Queryable).GetMethod(
            "Where", BindingFlags.Static,
            null,
            new Type[] {typeof (IQueryable<T>), typeof (Expression<Func<T, bool>>)},
            null);

So has anyone an idea how I can find the 'right' generic method via reflection. For example the right version of the 'Where'-method on the Queryable-class?

Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You can somewhat elegantly select a specific generic overload of a method at compile-time, without passing any strings to run-time searches like the other answers here do.

Static Methods

Suppose you have multiple static methods of the same name like:

public static void DoSomething<TModel>(TModel model)

public static void DoSomething<TViewModel, TModel>(TViewModel viewModel, TModel model)

// etc

If you create an Action or Func that matches the generic count and parameter count of the overload you're looking for, you can select it at compile-time with relatively few acrobatics.

Example: Select the first method - returns void, so use an Action, takes one generic. We use object to avoid specifying type just yet:

var method = new Action<object>(MyClass.DoSomething<object>);

Example: Select the second method - returns void, so Action, 2 generic types so use type object twice, once for each of the 2 generic parameters:

var method = new Action<object, object>(MyClass.DoSomething<object, object>);

You just got the method you wanted without doing any crazy plumbing, and no run-time searching or usage of risky strings.

MethodInfo

Typically in Reflection you want the MethodInfo object, which you can also get in a compile-safe way. This is when you pass the actual generic types you want to use in your method. Assuming you wanted the second method above:

var methodInfo = method.Method.MakeGenericMethod(type1, type2);

There's your generic method without any of the reflection searching or calls to GetMethod(), or flimsy strings.

Static Extension Methods

The specific example you cite with Queryable.Where overloads forces you to get a little fancy in the Func definition, but generally follows the same pattern. The signature for the most commonly used Where() extension method is:

public static IQueryable<TModel> Where<TModel>(this IQueryable<TModel>, Expression<Func<TModel, bool>>)

Obviously this will be slightly more complicated - here it is:

var method = new Func<IQueryable<object>,
                      Expression<Func<object, bool>>,
                      IQueryable<object>>(Queryable.Where<object>);

var methodInfo = method.Method.MakeGenericMethod(modelType);

Instance Methods

Incorporating Valerie's comment - to get an instance method, you'll need to do something very similar. Suppose you had this instance method in your class:

public void MyMethod<T1>(T1 thing)

First select the method the same way as for statics:

var method = new Action<object>(MyMethod<object>);

Then call GetGenericMethodDefinition() to get to the generic MethodInfo, and finally pass your type(s) with MakeGenericMethod():

var methodInfo = method.Method.GetGenericMethodDefinition().MakeGenericMethod(type1);

Decoupling MethodInfo and Parameter Types

This wasn't requested in the question, but once you do the above you may find yourself selecting the method in one place, and deciding what types to pass it in another. You can decouple those 2 steps.

If you're uncertain of the generic type parameters you're going to pass in, you can always acquire the MethodInfo object without them.

Static:

var methodInfo = method.Method;

Instance:

var methodInfo = method.Method.GetGenericMethodDefinition();

And pass that on to some other method that knows the types it wants to instantiate and call the method with - for example:

processCollection(methodInfo, type2);

...

protected void processCollection(MethodInfo method, Type type2)
{
    var type1 = typeof(MyDataClass);
    object output = method.MakeGenericMethod(type1, type2).Invoke(null, new object[] { collection });
}

One thing this especially helps with is selecting a specific instance method of a class, from inside the class, then exposing that to outside callers who need it with various types later on.

Addendum

A number of comments below say they cannot get this to work. It might not be surprising that I don't often have to select a generic method like this, but I happen to be doing so today, in well-tested code used behind the scenes all the time, so I thought I'd provide that real-world example - and perhaps it will help those who struggle to get this to work.

C# lacks a Clone method, so we have our own. It can take a number of arguments, including those that explain how to recursively copy IEnumerable properties inside the source object.

The method that copies an IEnumerable is named CopyList, and looks like this:

public static IEnumerable<TTo> CopyList<TTo>(
    IEnumerable<object> from,
    Func<PropertyInfo, bool> whereProps,
    Dictionary<Type, Type> typeMap
)
    where TTo : new()
{

To complicate things (and flex the muscles of this approach), it has several overloads, like this one:

public static IEnumerable<TTo> CopyList<TTo>(
    IEnumerable<object> from,
    Dictionary<Type, Type> typeMap
)
    where TTo : new()
{

So, we've got several (I'm only showing you 2, but there are more in the code) method signatures. They have the same number of Generic arguments, but a different number of method arguments. The names are identical. How are we possibly going to call the right method? Begin the C# ninjaing!

var listTo = ReflectionHelper.GetIEnumerableType(
    fromValue.GetType());

var fn = new Func<
    IEnumerable<object>,
    Func<PropertyInfo, bool>,
    Dictionary<Type, Type>,
    IEnumerable<object>>(
        ModelTransform.CopyList<object>);

var copyListMethod = fn.GetMethodInfo()
    .GetGenericMethodDefinition()
    .MakeGenericMethod(listTo);

copyListMethod.Invoke(null,
    new object[] { fromValue, whereProps, typeMap });

The first line uses a helper method we'll come back to, but all it's doing is getting the generic type of the IEnumerable list in this property, and assigning it to listTo. The next line is where we really begin performing this trick, where we lay out a Func with adequate parameters to match up with the specific CopyList() overload we intend to grab. Specifically, the CopyList() we want has 3 arguments, and returns IEnumerable<TTo>. Remember that Func takes its return type as its last generic arg, and that we're substituting object wherever there's a generic in the method we intend to grab. But, as you can see in this example, we do not need to substitute object anywhere else. For example, we know we want to pass a where clause that accepts a PropertyInfo and returns true/false (bool), and we just say those types right in the Func.

As the constructor arg to the Func, we pass CopyList() - but remember that the name CopyList is vague because of the method overloads. What's really cool is that C# is doing the hard work for you right now, by looking at the Func args, and identifying the right one. In fact, if you get the types or number of args wrong, Visual Studio will actually mark the line with an error:

No overload for 'CopyList' matches delegate 'Func...'

It's not smart enough to tell you what exactly you need to fix, but if you see that error you're close - you need to carefully double-check the args and return type and match them up exactly, replacing Generic args with object.

On the third line, we call the C# built-in .GetMethodInfo() and then .MakeGeneric(listTo). We have only one Generic to set for this, so we pass that in as listTo. If we had 2, we'd pass 2 args here. These Type args are replacing the object substitutions we made earlier.

And that's it - we can call copyListMethod(), with no strings, fully compile-safe. The final line makes the call, first passing null because it's a static method, then an object[] array with the 3 args. Done.

I said I'd come back to the ReflectionHelper method. Here it is:

public static Type GetIEnumerableType(Type type)
{
    var ienumerable = type.GetInterface(typeof(System.Collections.Generic.IEnumerable<>).FullName);
    var generics = ienumerable.GetGenericArguments();
    return generics[0];
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...