关于c#:选择带反射的右通用方法

Select Right Generic Method with Reflection

我想通过反射选择正确的通用方法,然后调用它。

通常这很容易。例如

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

但是,当方法有不同的泛型重载时,问题就开始了。例如,System.Linq.Queryable-Class中的静态方法。"Where"方法有两种定义

1
2
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)

这意味着getmethod不起作用,因为它无法解除这两个方法的配置。所以我想选择一个合适的。

到目前为止,根据我的需要,我通常只采用第一种或第二种方法。这样地:

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

然而,我对此并不满意,因为我做了一个巨大的假设,即第一种方法是正确的。我更希望通过参数类型找到正确的方法。但我不知道怎么做。

我试过通过"类型",但没用。

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

有人知道我如何通过反射找到"正确"的通用方法吗?例如,可查询类上"Where"方法的正确版本?


您可以在编译时优雅地选择某个方法的特定泛型重载,而不必像这里的其他答案那样向运行时搜索传递任何字符串。

静态方法

假设您有多个同名的静态方法,例如:

1
2
3
4
5
public static void DoSomething<TModel>(TModel model)

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

// etc

如果创建的操作或func与要查找的重载的常规计数和参数计数相匹配,则可以在编译时使用相对较少的技巧选择它。

示例:选择第一个方法-返回void,因此使用一个操作,采用一个泛型。我们使用对象来避免指定类型:

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

示例:选择第二个方法-返回void,so action,2个泛型类型,因此对2个泛型参数中的每一个使用类型对象两次:

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

你只是得到了你想要的方法,没有做任何疯狂的管道,也没有运行时搜索或使用风险字符串。

方法信息

通常,在反射中,您需要MethodInfo对象,您也可以以一种编译安全的方式获得该对象。这是传递要在方法中使用的实际泛型类型的时候。假设您需要上述第二种方法:

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

有一个通用方法没有任何反射搜索或对getMethod()或脆弱字符串的调用。

静态扩展方法

使用queryable引用的具体示例,其中重载迫使您对func定义有点兴趣,但通常遵循相同的模式。最常用的where()扩展方法的签名是:

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

显然,这会稍微复杂一点——这里是:

1
2
3
4
5
var method = new Func<IQueryable<object>,
                      Expression<Func<object, bool>>,
                      IQueryable<object>>(Queryable.Where<object>);

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

实例方法

结合Valerie的评论——要获得一个实例方法,您需要做一些非常类似的事情。假设您的类中有这个实例方法:

1
public void MyMethod<T1>(T1 thing)

首先选择与静态相同的方法:

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

然后调用GetGenericMethodDefinition()来获取通用的methodinfo,最后用MakeGenericMethod()传递您的类型:

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

分离MethodInfo和参数类型

这在问题中没有被要求,但是一旦你做了以上的事情,你可能会发现你自己在一个地方选择了方法,并决定在另一个地方传递它的类型。您可以将这两个步骤分离。

如果您不确定要传入的泛型类型参数,那么总是可以在没有它们的情况下获取MethodInfo对象。

静态的:

1
var methodInfo = method.Method;

实例:

1
var methodInfo = method.Method.GetGenericMethodDefinition();

并将其传递给其他一些方法,这些方法知道它想要实例化的类型,并用以下方法调用该方法-例如:

1
2
3
4
5
6
7
8
9
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 });
}

这尤其有助于从类内部选择类的特定实例方法,然后将其公开给以后需要各种类型的外部调用方。

编辑:清理了解释,合并了Valerie的实例方法示例。


可以做到,但不漂亮!

例如,要获得问题中提到的第一个Where过载,您可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var where1 = typeof(Queryable).GetMethods()
                 .Where(x => x.Name =="Where")
                 .Select(x => new { M = x, P = x.GetParameters() })
                 .Where(x => x.P.Length == 2
                             && x.P[0].ParameterType.IsGenericType
                             && x.P[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
                             && x.P[1].ParameterType.IsGenericType
                             && x.P[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))
                 .Select(x => new { x.M, A = x.P[1].ParameterType.GetGenericArguments() })
                 .Where(x => x.A[0].IsGenericType
                             && x.A[0].GetGenericTypeDefinition() == typeof(Func<,>))
                 .Select(x => new { x.M, A = x.A[0].GetGenericArguments() })
                 .Where(x => x.A[0].IsGenericParameter
                             && x.A[1] == typeof(bool))
                 .Select(x => x.M)
                 .SingleOrDefault();

或者如果你想要第二个超负荷:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var where2 = typeof(Queryable).GetMethods()
                 .Where(x => x.Name =="Where")
                 .Select(x => new { M = x, P = x.GetParameters() })
                 .Where(x => x.P.Length == 2
                             && x.P[0].ParameterType.IsGenericType
                             && x.P[0].ParameterType.GetGenericTypeDefinition() == typeof(IQueryable<>)
                             && x.P[1].ParameterType.IsGenericType
                             && x.P[1].ParameterType.GetGenericTypeDefinition() == typeof(Expression<>))
                 .Select(x => new { x.M, A = x.P[1].ParameterType.GetGenericArguments() })
                 .Where(x => x.A[0].IsGenericType
                             && x.A[0].GetGenericTypeDefinition() == typeof(Func<,,>))
                 .Select(x => new { x.M, A = x.A[0].GetGenericArguments() })
                 .Where(x => x.A[0].IsGenericParameter
                             && x.A[1] == typeof(int)
                             && x.A[2] == typeof(bool))
                 .Select(x => x.M)
                 .SingleOrDefault();


这个问题大约2岁,但我想出了一个(我认为是)优雅的解决方案,我想我会在StackOverflow与这里的优秀员工分享它。希望它能帮助那些通过各种搜索查询到达这里的人。

正如海报所述,问题在于获得正确的通用方法。例如,Linq扩展方法可能有大量的重载,类型参数嵌套在其他泛型类型中,所有这些都用作参数。我想这样做:

1
2
3
4
5
6
7
8
9
10
11
var where = typeof(Enumerable).GetMethod(
 "Where",
  typeof(IQueryable<Refl.T1>),
  typeof(Expression<Func<Refl.T1, bool>>
);

var group = typeof(Enumerable).GetMethod(
 "GroupBy",
  typeof(IQueryable<Refl.T1>),
  typeof(Expression<Func<Refl.T1, Refl.T2>>
);

如您所见,我已经创建了一些存根类型"T1"和"T2",在类"refl"中嵌套了类(一个静态类,其中包含我所有的各种反射实用程序扩展函数等)。它们用作类型参数通常所在位置的占位符。上面的示例分别对应于获取以下LINQ方法:

1
2
Enumerable.Where(IQueryable<TSource> source, Func<TSource, bool> predicate);
Enumerable.GroupBy(IQueryable<Source> source, Func<TSource, TKey> selector);

因此,很明显,在这两个调用中,Refl.T1会去TSource会去的地方;Refl.T2表示TKey参数。TX类是这样声明的:

1
2
3
4
5
6
static class Refl {
  public sealed class T1 { }
  public sealed class T2 { }
  public sealed class T3 { }
  // ... more, if you so desire.
}

使用三个TX类,代码可以识别包含最多三个泛型类型参数的方法。

下一点魔法是实现通过GetMethods()进行搜索的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public static MethodInfo GetMethod(this Type t, string name, params Type[] parameters)
{
    foreach (var method in t.GetMethods())
    {
        // easiest case: the name doesn't match!
        if (method.Name != name)
            continue;
        // set a flag here, which will eventually be false if the method isn't a match.
        var correct = true;
        if (method.IsGenericMethodDefinition)
        {
            // map the"private" Type objects which are the type parameters to
            // my public"Tx" classes...
            var d = new Dictionary<Type, Type>();
            var args = method.GetGenericArguments();
            if (args.Length >= 1)
                d[typeof(T1)] = args[0];
            if (args.Length >= 2)
                d[typeof(T2)] = args[1];
            if (args.Length >= 3)
                d[typeof (T3)] = args[2];
            if (args.Length > 3)
                throw new NotSupportedException("Too many type parameters.");

            var p = method.GetParameters();
            for (var i = 0; i < p.Length; i++)
            {
                // Find the Refl.TX classes and replace them with the
                // actual type parameters.
                var pt = Substitute(parameters[i], d);
                // Then it's a simple equality check on two Type instances.
                if (pt != p[i].ParameterType)
                {
                    correct = false;
                    break;
                }
            }
            if (correct)
                return method;
        }
        else
        {
            var p = method.GetParameters();
            for (var i = 0; i < p.Length; i++)
            {
                var pt = parameters[i];
                if (pt != p[i].ParameterType)
                {
                    correct = false;
                    break;
                }
            }
            if (correct)
                return method;
        }
    }
    return null;
}

上面的代码完成了大部分工作——它迭代特定类型中的所有方法,并将它们与要搜索的给定参数类型进行比较。但是等等!那"替代"功能呢?这是一个很好的小递归函数,可以搜索整个参数类型树——毕竟,参数类型本身可以是一个泛型类型,它可能包含Refl.TX类型,必须替换为对我们隐藏的"real"类型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Type Substitute(Type t, IDictionary<Type, Type> env )
{
    // We only really do something if the type
    // passed in is a (constructed) generic type.
    if (t.IsGenericType)
    {
        var targs = t.GetGenericArguments();
        for(int i = 0; i < targs.Length; i++)
            targs[i] = Substitute(targs[i], env); // recursive call
        t = t.GetGenericTypeDefinition();
        t = t.MakeGenericType(targs);
    }
    // see if the type is in the environment and sub if it is.
    return env.ContainsKey(t) ? env[t] : t;
}


您可能会发现另一个有用的解决方案-有可能得到一个基于Expression.CallMethodInfo,它已经具有过载解决的逻辑。

例如,如果您需要获得一些特定的Enumerable.Where方法,可以使用以下代码来完成:

1
2
var mi = Expression.Call(typeof (Enumerable),"Where", new Type[] {typeof (int)},
            Expression.Default(typeof (IEnumerable<int>)), Expression.Default(typeof (Func<int, int, bool>))).Method;

示例中的第三个参数-描述泛型参数的类型,以及所有其他参数-参数的类型。

同样,甚至可以获得非静态对象的通用方法,只需将第一个参数从typeof (YourClass)更改为Expression.Default(typeof (YourClass))

实际上,我在我的.NET反射API插件中使用了这种方法。你可以检查一下这里的工作原理


让编译器为您做:

1
2
var fakeExp = (Expression<Func<IQueryable<int>, IQueryable<int>>>)(q => q.Where((x, idx) => x> 2));
var mi = ((MethodCallExpression)fakeExp.Body).Method.GetGenericMethodDefinition();

对于带索引的Where,或者只需在Where表达式中为不带索引的Where表达式省略第二个参数。


除了@mboros的回答。

可以避免使用此帮助器方法编写复杂的泛型参数:

1
2
3
4
public static MethodInfo GetMethodByExpression<Tin, Tout>(Expression<Func<IQueryable<Tin>, IQueryable<Tout>>> expr)  
{  
    return (expr.Body as MethodCallExpression).Method;  
}

用途:

1
var where = GetMethodByExpression<int, int>(q => q.Where((x, idx) => x > 2));

1
var select = GetMethodByExpression<Person, string>(q => q.Select(x => x.Name));


使用DynamicMethods.GenericMethodInvokerMethod,GetMethod不足以用于泛型


我有一个类似的问题,我想我会把我的解决方案贴在这里。我尝试调用几个函数:

1
2
3
4
5
6
p.Foo<Klass1>(true)
p.Foo<Klass2>(true)
p.Foo<Klass3>(true)
bool k1 = p.Bar<Klass1>()
bool k2 = p.Bar<Klass2>()
bool k3 = p.Bar<Klass3>()

我的解决方案:

1
2
3
4
5
6
7
public static TAction RemapGenericMember<TAction>(object parent, Type target, TAction func) where TAction : Delegate {
    var genericMethod = func?.Method?.GetGenericMethodDefinition()?.MakeGenericMethod(target);
    if (genericMethod.IsNull()) {
        throw new Exception($"Failed to build generic call for '{func.Method.Name}' with generic type '{target.Name}' for parent '{parent.GetType()}'");
    }
    return (TAction)genericMethod.CreateDelegate(typeof(TAction), parent);
}

现在我可以打电话给:

1
2
3
4
foreach(var type in supportedTypes) {
   InvokeGenericMember<Action<bool>>(p, type, Foo<object>)(true);
   bool x = InvokeGenericMember<Function<bool>>(p, type, Bar<object>)();
}

我做了一个小帮手Func:

1
2
3
4
5
6
7
8
9
10
11
12
Func<Type, string, Type[], Type[], MethodInfo> getMethod = (t, n, genargs, args) =>
    {
        var methods =
            from m in t.GetMethods()
            where m.Name == n && m.GetGenericArguments().Length == genargs.Length
            let mg = m.IsGenericMethodDefinition ? m.MakeGenericMethod(genargs) : m
            where mg.GetParameters().Select(p => p.ParameterType).SequenceEqual(args)
            select mg
            ;

        return methods.Single();
    };

适用于简单非泛型:

1
var m_movenext = getMethod(typeof(IEnumerator), nameof(IEnumerator.MoveNext), Type.EmptyTypes, Type.EmptyTypes);

对于复杂的仿制药:

1
2
3
4
5
6
7
8
9
10
11
12
13
var t_source = typeof(fillin1);
var t_target = typeof(fillin2);
var m_SelectMany = getMethod(
           typeof(Enumerable),
           nameof(Enumerable.SelectMany),
           new[] {
               t_source,
               t_target
           },
           new[] {
               typeof(IEnumerable<>).MakeGenericType(t_source),
               typeof(Func<,>).MakeGenericType(t_source, typeof(IEnumerable<>).MakeGenericType(t_target))
           });


我发现了在使用反射调用方法时使用iQuery表达式的最简单方法。请参见以下代码:

您可以根据需要使用IQuerable表达式。

1
2
3
4
var attributeName ="CarName";
var attributeValue ="Honda Accord";

carList.FirstOrDefault(e => e.GetType().GetProperty(attributeName).GetValue(e, null) as string== attributeValue);

安塔米尔的答案对我来说非常有用,但它有一个缺陷,那就是它不能验证找到的方法中参数的数量是否与提供通用类型和具体类型混合时传入的类型的数量相匹配。

例如,如果您运行:

1
type.GetMethod("MyMethod",typeof(Refl.T1),typeof(bool))

不能区分两种方法:

1
2
MyMethod<T>(T arg1)
MyMethod<T>(T arg1, bool arg2)

这两个电话是:

1
var p = method.GetParameters();

应改为:

1
2
3
4
5
6
var p = method.GetParameters();  
if (p.Length != parameters.Length)
{
    correct = false;
    continue;
}

此外,现有的"break"行都应为"continue"。


当您在编译时知道方法名时,ChrisMoschini的答案是好的。如果我们在运行时得到方法名,安塔米尔的答案是可行的,但这是一个过分的错误。

我使用的是另一种方法,我从.NET函数Expression.Call获得了使用reflector的灵感,后者从字符串中选择了正确的泛型方法。

1
2
3
4
5
6
7
8
9
10
11
12
public static MethodInfo GetGenericMethod(Type declaringType, string methodName, Type[] typeArgs, params Type[] argTypes) {
    foreach (var m in from m in declaringType.GetMethods()
                        where m.Name == methodName
                            && typeArgs.Length == m.GetGenericArguments().Length
                            && argTypes.Length == m.GetParameters().Length
                        select m.MakeGenericMethod(typeArgs)) {
        if (m.GetParameters().Select((p, i) => p.ParameterType == argTypes[i]).All(x => x == true))
            return m;
    }

    return null;
}

用途:

1
var m = ReflectionUtils.GetGenericMethod(typeof(Queryable),"Where", new[] { typeof(Person) }, typeof(IQueryable<Person>), typeof(Expression<Func<Person, bool>>));

如果您当时只需要泛型方法定义或只是不知道T类型,则可以使用一些伪类型,然后去掉泛型的信息:

1
2
var m = ReflectionUtils.GetGenericMethod(typeof(Queryable),"Where", new[] { typeof(object) }, typeof(IQueryable<object>), typeof(Expression<Func<object, bool>>));
m = m.GetGenericMethodDefinition();