使用反射选择正确的通用方法

45

我希望能够通过反射选择正确的泛型方法并调用它。

通常情况下这很容易实现。例如:

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

然而,当方法存在不同的泛型重载时,问题就开始出现了。例如在 System.Linq.Queryable 类中的静态方法,'Where' 方法有两个定义。

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 不起作用,因为它无法区分两者。因此,我想选择正确的方法。
到目前为止,我经常根据需要选择第一个或第二个方法。就像这样:
var method = typeof (Queryable).GetMethods().First(m => m.Name == "Where");
var typedMethod = method.MakeGenericMethod(theTypeToInstantiate);

然而,我对此并不满意,因为我做出了一个巨大的假设,即第一种方法是正确的。我更希望通过参数类型找到正确的方法。但我无法弄清楚如何做到这一点。

我尝试使用传递 'types' 的方式,但它没有起作用。

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

有没有人知道我如何通过反射找到“正确的”泛型方法。例如,Queryable类上的“Where”方法的正确版本是什么?

13个回答

54

您可以在不将任何字符串传递到运行时搜索的情况下,在编译时相对优美地选择特定的方法通用重载,就像其他答案所做的那样。

静态方法

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

public static void DoSomething<TModel>(TModel model)

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

// etc

如果您创建的Action或Func与您正在寻找的重载的泛型计数和参数计数匹配,您可以在编译时使用相对较少的技巧选择它。

示例:选择第一个方法 - 返回void,因此使用Action,需要一个泛型。我们使用object来避免立即指定类型:

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

示例:选择第二种方法 - 返回void,因此使用Action,两个通用类型所以将type object分别用于两个通用参数:

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

您得到了所需的方法,无需进行任何疯狂的管道操作,也不需要运行时搜索或使用风险字符串。

MethodInfo

通常在反射中,您需要MethodInfo对象,您还可以以编译安全的方式获取它。这是当您传递要在方法中使用的实际泛型类型时发生的情况。假设您想要上面第二个方法:

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

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

静态扩展方法

你提到的具体例子 Queryable.Where 迫使你在 Func 定义中变得有点花哨,但一般都是按照相同的模式进行的。最常用的 Where() 扩展方法的签名如下:

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

显然这会稍微复杂些-下面是它:

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

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

实例方法

根据Valerie的评论,要获取一个实例方法,你需要做类似的事情。假设你在你的类中有这个实例方法:

public void MyMethod<T1>(T1 thing)

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

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

接着调用 GetGenericMethodDefinition() 来获取泛型MethodInfo,最后使用 MakeGenericMethod() 传递你的类型参数:

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

解耦MethodInfo和参数类型

虽然问题中没有提到,但是在执行上述操作后,你可能会发现自己需要在一个地方选择方法,在另一个地方决定传递什么类型的参数。你可以将这两个步骤解耦。

如果你不确定要传递的泛型类型参数,你可以在不使用它们的情况下获取MethodInfo对象。

静态:

var methodInfo = method.Method;

实例:

var methodInfo = method.Method.GetGenericMethodDefinition();

然后将其传递给其他知道要实例化的类型并调用该方法的方法-例如:

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 });
}

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

附录

下面的一些评论说他们无法使其工作。也许并不奇怪,我并不经常像这样选择通用方法,但今天我正在这样做,在经过充分测试的代码中,该代码一直在幕后使用,因此我想提供一个真实的例子——也许它将有助于那些难以使其正常工作的人。

C#缺少Clone方法,因此我们有自己的方法。它可以接受多个参数,包括解释如何递归复制源对象中的IEnumerable属性的参数。

复制IEnumerable的方法名为CopyList,看起来像这样:

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

为了让事情变得更加复杂(并展示这种方法的实力),它有几个重载,像这个:

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

所以,我们有几个(我只向您展示了2个,但代码中还有更多)方法签名。它们具有相同数量的通用参数,但具有不同数量的方法参数。名称相同。我们该如何调用正确的方法?开始C#忍者攻击吧!

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 });

第一行使用一个我们稍后会回来的帮助方法,但它所做的只是获取该属性中IEnumerable列表的泛型类型,并将其赋值给listTo。下一行是我们真正开始执行这个技巧的地方,我们布置了一个Func,并提供了足够的参数以匹配我们打算抓取的特定CopyList()重载版本。具体而言,我们想要的CopyList()有3个参数,并返回IEnumerable<TTo>。请记住,Func将其返回类型作为其最后的泛型参数,并且我们在打算抓取的方法中无论何时都可以用object替换泛型。但是,正如您在此示例中看到的那样,我们不需要在任何其他地方替换object。例如,我们知道我们想要传递一个接受PropertyInfo并返回true/false(bool)的where子句,我们只需在Func中直接指定这些类型。

作为Func的构造函数参数,我们传递CopyList()——但请记住,由于方法重载,名称CopyList是含糊不清的。真正酷的是,C#现在正在为您做这项艰巨的工作,通过查看Func参数并确定正确的参数。事实上,如果您在类型或参数数量方面犯了错误,Visual Studio实际上会用错误标记该行:

 

没有匹配委托 'Func...'的'CopyList'重载

它不够聪明,不能告诉你需要修复什么,但是如果您看到该错误,就接近了——您需要仔细检查参数和返回类型,并准确匹配它们,并将通用参数替换为object。

第三行,我们调用C#内置的.GetMethodInfo(),然后是.MakeGeneric(listTo)。对于这个方法,我们只有一个泛型要设置,所以我们将其作为listTo传递进去。如果有2个,我们在此处传递2个参数。这些参数替换了我们之前进行的替换。

就这样,我们可以调用copyListMethod(),无需字符串,完全编译安全。最后一行进行了调用,首先传递null,因为它是一个静态方法,然后是一个包含3个参数的数组。 完成。

我说过我会回来介绍ReflectionHelper方法。下面就是它:

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


8
我不知道为什么这不是最佳答案!这很简单且类型安全。我稍微修改了一下就能在我们的代码中使用。我之前调用的是非静态方法,所以必须先获取泛型定义再转换成泛型方法:var methodInfo = method.Method.GetGenericMethodDefinition().MakeGenericMethod(type1); - Valerie
3
我同意Valerie的观点。这是我现在做这件事情的方式。不再出现打错字符串导致空引用异常的情况...... 对于原始问题,需要使用Func<>而不是Action<>,并且必须调用GetGenericMethodDefinition()方法。 - mheyman
1
在调用Queryable.Where时,我遇到了InvalidOperationException异常,该异常是由于调用MakeGenericMethod引起的:System.Linq.IQueryable1[System.Object] Where[Object](System.Linq.IQueryable1[System.Object], System.Linq.Expressions.Expression1[System.Func2[System.Object,System.Boolean]])不是一个泛型方法定义。MakeGenericMethod只能在MethodBase.IsGenericMethodDefinition为true的方法上调用。 - Tsahi Asher
这在存在重载方法的情况下如何工作(其中没有类型重载,只有参数重载)? - MBoros
@MBoros请看一下我回答中的Where示例。Where有多个参数重载,可以使用相同的泛型。我在这里提出的方法旨在解决您描述的情况。如果时间允许,我会添加另一个没有泛型的方法示例。 - Chris Moschini
显示剩余4条评论

24

可以做到,但不太美观!

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

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();

或者,如果你想要使用第二种重载:

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();

1
如果你想调用生成的 MethodInfo,你需要在其上调用 .MakeGenericMethod() 并调用其返回的 MethodInfo - Tsahi Asher
如果你只有特定的参数,你该如何找到任何方法?如果你没有硬编码这3种类型(可查询、表达式、函数),它会是什么样子? - Riki

18
这个问题已经有2年了,但是我想到了一个优雅的解决方案,并且想与StackOverflow社区分享。希望它能帮助那些通过各种搜索查询到达这里的人。
正如发帖者所述,问题在于获取正确的通用方法。例如,LINQ扩展方法可能有大量重载,其中包含嵌套在其他泛型类型中的类型参数,所有这些都用作参数。我想做类似这样的事情:
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"中的类("Refl"是一个静态类,包含所有反射实用程序扩展函数等)。它们作为类型参数应该放置的位置的占位符。上面的示例分别对应获取以下LINQ方法:

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 类的声明如下:

static class Refl {
  public sealed class T1 { }
  public sealed class T2 { }
  public sealed class T3 { }
  // ... more, if you so desire.
}

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

接下来的魔法是通过GetMethods()实现搜索的函数:

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;
}

上面的代码完成了大部分工作--它遍历特定类型中的所有方法,并将它们与给定的参数类型进行比较以搜索。但是等等!那个“substitute”函数呢?那是一个很好的递归函数,将搜索整个参数类型树--毕竟,参数类型本身可以是泛型类型,其中可能包含Refl.TX类型,必须将其替换为我们无法访问的“真实”类型参数。

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;
}

很棒的解决方案!我给了这个答案500分的奖励,以表彰其应得的认可。 - Rex M
对于比原始问题更复杂的情况,我们需要获取MethodInfo并且只有一些泛型参数在那个时候是已知的,这为消费开发者提供了一个极好的使用案例。代码量易于封装和覆盖单元测试。 - Rex M
@RexM,你能提供一个例子吗?也许这应该作为一个单独的问题。在获取MethodInfo对象时,只要知道泛型参数的一部分,两种方法都可以很好地处理 - 我会更新我的答案来说明。我怀疑,除非在编译时不确定方法的名称,否则这种方法的性能较差,而且没有提供额外的好处。 - Chris Moschini
1
@ChrisMoschini,非常抱歉!我误以为你的回答是被接受的 :( - atanamir
这个代码 var where = typeof(Enumerable).GetMethod( "Where", typeof(IQueryable<Refl.T1>), typeof(Expression<Func<Refl.T1, bool>> ) 能编译通过吗?据我所知,我们必须传递一个 Type 数组,这里没有定义 params。而且我认为在这里使用虚拟类是不起作用的。一旦使用虚拟类,获得的方法应该接收完全相同的虚拟类型参数,这是不可接受的。很抱歉晚了才发表评论,但我认为答案中的所有错误都应该被编辑。 - Hopeless
显示剩余6条评论

5

还有一个解决方案可能会对您有用 - 可以基于 Expression.Call 获取一个已经具备重载解析逻辑的 MethodInfo

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

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 Reflection API插件中使用了这种方法。您可以在此处查看其工作原理。


3
让编译器替你完成:
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 表达式中省略第二个参数。


3

当您在编译时知道方法名称时,Chris Moschini的答案很好。Antamir的答案适用于我们在运行时获取方法名称,但这样做有些过度。

我正在使用另一种方式,得到灵感是使用.NET函数Expression.Call中的反射器,它从字符串中选择正确的泛型方法。

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;
}

用法:

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

如果您只需要通用方法定义或者在编写代码时不知道类型T,可以使用一些虚假的类型,然后去除泛型信息:

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

2

这并不完全正确(现在?)。请参考Илья Любашов下面的答案,其中提供了一个可行的示例(虽然解释有点简略)。 - SvenL

2

除了@MBoros的答案外。

您可以使用此辅助方法避免编写复杂的通用参数:

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

使用方法:

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

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

1
我写了一个小助手函数:

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();
    };

适用于简单的非泛型:

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

对于复杂的泛型:

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)) 
           });

1
var firstGenericParam = Type.MakeGenericMethodParameter(0);
var firstParam = typeof(IQueryable<>).MakeGenericType(firstGenericParam);
var funcType = typeof(Func<,>).MakeGenericType(firstGenericParam, typeof(bool));
//var funcType = typeof(Func<,,>).MakeGenericType(firstGenericParam, typeof(int), typeof(bool)); //for second version 
var secondParam = typeof(Expression<>).MakeGenericType(funcType);
var method = typeof(Queryable).GetMethod(nameof(Queryable.Where), new Type[] { firstParam, secondParam });

1
虽然您的回答可能解决了问题,但是包括解释如何以及为什么解决问题将有助于提高您的帖子质量,并可能导致更多的赞。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。您可以编辑您的答案以添加解释,并指出适用的限制和假设。- 来自审核 - Adam Marshall
1
不确定为什么这个回答被投票否决,因为(在我看来)这是唯一一个真正回答了问题的答案。其他答案可能有更多的解释......但并没有回答如何选择正确的答案,除了使用linq等自己搜索。感谢@Илья Любашов,这帮了很大的忙。 - SvenL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接