获取泛型方法的GetMethod

54

我正在尝试检索Enumerable类型的Where方法的MethodInfo:

typeof (Enumerable).GetMethod("Where", new Type[] { 
     typeof(IEnumerable<>), 
     typeof(Func<,>) 
})

但是我得到了 null。我做错了什么?


1
获取 Func<T, bool> 的类型对我来说似乎是个障碍。 - Hans Passant
另一个完全重复的问题:使用反射选择正确的泛型方法 - nawfal
2个回答

55
那个之前的答案对于某些情况有效,但是:
  • 它无法处理嵌套的泛型类型,例如参数类型为 Action<IEnumerable<T>>。如果在字符串类型上搜索类型为 IEnumerable<>"Concat",它将把所有的 Action<> 都视为匹配,例如 string.Concat(IEnumerable<string>)string.Concat<T>(IEnumerable<T>) 都会匹配。真正需要的是递归处理嵌套的泛型类型,同时将所有泛型参数视为相互匹配而不考虑名称,同时不匹配具体类型。
  • 它返回第一个匹配的方法而不是抛出异常,如果结果是模棱两可的,就像 type.GetMethod() 一样。所以,你可能会得到你想要的方法,如果你很幸运的话,或者你可能不会得到。
  • 有时需要指定 BindingFlags 以避免歧义,例如当派生类方法“隐藏”基类方法时。通常情况下,您希望找到基类方法,但在您知道要查找的方法在派生类中时,您不希望这样做。或者,您可能知道您正在查找静态方法 vs 实例方法、公共方法 vs 私有方法等,如果不精确匹配,就不想匹配。
  • 它没有解决 type.GetMethods() 的另一个主要缺陷,即在查找接口类型上的方法时,它也不会搜索基本接口。好吧,也许这有点挑剔,但对于我来说,这是 GetMethods() 的另一个重大缺陷。
  • 调用 type.GetMethods() 是低效的,type.GetMember(name, MemberTypes.Method, ...) 将仅返回具有匹配名称的方法,而不是类型中的所有方法。
  • 最后,名称 GetGenericMethod() 可能会误导,因为你可能正在尝试找到一个非泛型方法,该方法由于泛型声明类型中存在类型参数而出现在参数类型中。
下面是一个可以解决所有这些问题并可用作有缺陷的 GetMethod() 的通用替代版本。请注意,提供了两个扩展方法,一个带有 BindingFlags,一个没有(为方便起见)。
/// <summary>
/// Search for a method by name and parameter types.  
/// Unlike GetMethod(), does 'loose' matching on generic
/// parameter types, and searches base interfaces.
/// </summary>
/// <exception cref="AmbiguousMatchException"/>
public static MethodInfo GetMethodExt(  this Type thisType, 
                                        string name, 
                                        params Type[] parameterTypes)
{
    return GetMethodExt(thisType, 
                        name, 
                        BindingFlags.Instance 
                        | BindingFlags.Static 
                        | BindingFlags.Public 
                        | BindingFlags.NonPublic
                        | BindingFlags.FlattenHierarchy, 
                        parameterTypes);
}

/// <summary>
/// Search for a method by name, parameter types, and binding flags.  
/// Unlike GetMethod(), does 'loose' matching on generic
/// parameter types, and searches base interfaces.
/// </summary>
/// <exception cref="AmbiguousMatchException"/>
public static MethodInfo GetMethodExt(  this Type thisType, 
                                        string name, 
                                        BindingFlags bindingFlags, 
                                        params Type[] parameterTypes)
{
    MethodInfo matchingMethod = null;

    // Check all methods with the specified name, including in base classes
    GetMethodExt(ref matchingMethod, thisType, name, bindingFlags, parameterTypes);

    // If we're searching an interface, we have to manually search base interfaces
    if (matchingMethod == null && thisType.IsInterface)
    {
        foreach (Type interfaceType in thisType.GetInterfaces())
            GetMethodExt(ref matchingMethod, 
                         interfaceType, 
                         name, 
                         bindingFlags, 
                         parameterTypes);
    }

    return matchingMethod;
}

private static void GetMethodExt(   ref MethodInfo matchingMethod, 
                                    Type type, 
                                    string name, 
                                    BindingFlags bindingFlags, 
                                    params Type[] parameterTypes)
{
    // Check all methods with the specified name, including in base classes
    foreach (MethodInfo methodInfo in type.GetMember(name, 
                                                     MemberTypes.Method, 
                                                     bindingFlags))
    {
        // Check that the parameter counts and types match, 
        // with 'loose' matching on generic parameters
        ParameterInfo[] parameterInfos = methodInfo.GetParameters();
        if (parameterInfos.Length == parameterTypes.Length)
        {
            int i = 0;
            for (; i < parameterInfos.Length; ++i)
            {
                if (!parameterInfos[i].ParameterType
                                      .IsSimilarType(parameterTypes[i]))
                    break;
            }
            if (i == parameterInfos.Length)
            {
                if (matchingMethod == null)
                    matchingMethod = methodInfo;
                else
                    throw new AmbiguousMatchException(
                           "More than one matching method found!");
            }
        }
    }
}

/// <summary>
/// Special type used to match any generic parameter type in GetMethodExt().
/// </summary>
public class T
{ }

/// <summary>
/// Determines if the two types are either identical, or are both generic 
/// parameters or generic types with generic parameters in the same
///  locations (generic parameters match any other generic paramter,
/// but NOT concrete types).
/// </summary>
private static bool IsSimilarType(this Type thisType, Type type)
{
    // Ignore any 'ref' types
    if (thisType.IsByRef)
        thisType = thisType.GetElementType();
    if (type.IsByRef)
        type = type.GetElementType();

    // Handle array types
    if (thisType.IsArray && type.IsArray)
        return thisType.GetElementType().IsSimilarType(type.GetElementType());

    // If the types are identical, or they're both generic parameters 
    // or the special 'T' type, treat as a match
    if (thisType == type || ((thisType.IsGenericParameter || thisType == typeof(T)) 
                         && (type.IsGenericParameter || type == typeof(T))))
        return true;

    // Handle any generic arguments
    if (thisType.IsGenericType && type.IsGenericType)
    {
        Type[] thisArguments = thisType.GetGenericArguments();
        Type[] arguments = type.GetGenericArguments();
        if (thisArguments.Length == arguments.Length)
        {
            for (int i = 0; i < thisArguments.Length; ++i)
            {
                if (!thisArguments[i].IsSimilarType(arguments[i]))
                    return false;
            }
            return true;
        }
    }

    return false;
}

请注意,IsSimilarType(Type)扩展方法可以被公开,并且可能单独使用。我知道,这个名称不太好 - 您可以想出一个更好的名称,但是解释它可能会变得非常冗长。此外,我通过检查'ref'和数组类型(匹配时忽略引用,但数组维数必须匹配)添加了另一个改进。

因此,这就是Microsoft应该做的方式。真的并不难。

是的,我知道,您可以使用Linq缩短一些逻辑,但我不是Linq在低级例程中的铁杆粉丝,除非Linq与原始代码一样易于理解,但通常情况下并不是这种情况。

如果您喜欢Linq,并且您必须使用它,您可以将IsSimilarType()的最内部部分替换为以下内容(将8行转换为1行):

if (thisArguments.Length == arguments.Length)
    return !thisArguments.Where((t, i) => !t.IsSimilarType(arguments[i])).Any();

最后一件事:如果你正在寻找一个带有通用参数的通用方法,例如Method<T>(T, T[]),那么你必须找到一个类型是通用参数(IsGenericParameter == true)的Type来传递参数类型(任何一个都可以,因为有“通配符”匹配)。然而,你不能只是做new Type() - 你必须找到一个真正的(TypeBuilder也行)。为了让这更容易,我添加了public class T声明,并在IsSimilarType()中添加了逻辑来检查它并匹配任何通用参数。如果你需要一个T[],只需使用T.MakeArrayType(1)


2
有点激烈了...所以总结一下这场辩论,这里提出的 GetMethodExt() 确实可以帮助查找方法,即使它们具有泛型参数,而现有的 GetMethod() 则不行。但是它有一个限制,就是调用它的类型必须与作为第二个参数传递的对象的类型匹配 - 如果第二个参数在继承树中更深,它将无法找到该方法。公平吗?看起来仍然是有用的代码(解决了我的用例),注意到了限制。大家都满意吗?Ken,把这个发布到 Github 上可能是个好主意。 - Chris Moschini
2
目前无法找到具有可选参数的方法。解决方案(如果将可选参数传递为null)是将IsSimilarType测试更改为if (!parameterInfos[i].IsOptional && !parameterInfos[i].ParameterType.IsSimilarType(parameterTypes[i]))需要进行其他代码更改以支持省略可选参数。 - Handcraftsman
1
对不起,@nawfal。 我已经超出了我的时间限制,我最初只是捐赠了一些代码来解决与此问题相关的所有问题(这是一个完整的C# 5.0编译器实现)。 我的慷慨并没有得到感谢,而是被粗鲁地骚扰,要求为其他人的可疑用例使其工作 - 更不用说半打成员错误地将该问题(不正确地)声明为重复。 现在,同一位成员想要更多的时间来教育他? 不用了,谢谢。 - Ken Beckett
2
@KenBeckett,我看你过于容易将事情当成个人问题。无论何种原因,这里没有人对你有任何个人恩怨,我们都感谢会员在这里所做的一点工作。尽管如此,这并不给任何人宣称虚假信息的权利。我们中的一些人正在指出你的错误之处。与其质疑我们的动机,不妨使用编辑功能纠正你的回答(如果你发现有错),或者给我们一个回复。 - nawfal
1
  1. 请告诉我你的回答中回答了我的哪个问题。
  2. 不,如果我知道答案,我会发帖的。无知不是罪过,粗鲁才是。只是因为我不知道答案,所以来这里问。
最后,贝克特,请在SO上四处寻找人们建议改进,并质疑有问题的答案(或其中的一部分)。这就是游戏规则。是的,我是那些因为你的回答真的很好而点赞的人之一。但是我有问题,不幸的是这不是懒惰,而是无知。
- nawfal
显示剩余13条评论

31

遗憾的是,泛型在.NET反射中得到的支持不够好。在这种情况下,您需要调用GetMethods方法,然后对结果集进行筛选以获取您要查找的方法。像以下这样的扩展方法应该可以解决问题。

public static class TypeExtensions
{
    private class SimpleTypeComparer : IEqualityComparer<Type>
    {
        public bool Equals(Type x, Type y)
        {
            return x.Assembly == y.Assembly &&
                x.Namespace == y.Namespace &&
                x.Name == y.Name;
        }

        public int GetHashCode(Type obj)
        {
            throw new NotImplementedException();
        }
    }

    public static MethodInfo GetGenericMethod(this Type type, string name, Type[] parameterTypes)
    {
        var methods = type.GetMethods();
        foreach (var method in methods.Where(m => m.Name == name))
        {
            var methodParameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();

            if (methodParameterTypes.SequenceEqual(parameterTypes, new SimpleTypeComparer()))
            {
                return method;
            }
        }

        return null;
    }
}

有了这个,以下代码将起作用:

typeof(Enumerable).GetGenericMethod("Where", new Type[] { typeof(IEnumerable<>), typeof(Func<,>) });

或者一行代码:返回 type.GetMethods().FirstOrDefault(m => m.Name == name && m.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameterTypes, new SimpleTypeComparer())); :) 在最后一个参数上添加 params 会更好。 - nawfal
不要在每次循环中创建一个SimpleTypeComparer实例,将其作为类的静态字段以提高效率。 - reggaeguitar
这个无法正确过滤嵌套泛型。 - Steve Andrews

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