Подтвердить что ты не робот

Метод GetMethod для общего метода

Я пытаюсь извлечь MethodInfo для метода Where для типа Enumerable:

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

но получите значение null. Что я делаю неправильно?

4b9b3361

Ответ 1

Тем не менее, предыдущий ответ работает в некоторых случаях:

  • Он не обрабатывает вложенные общие типы, такие как тип параметра Action<IEnumerable<T>>. Он будет обрабатывать все Action<> в качестве совпадений, например, string.Concat(IEnumerable<string>) и string.Concat<T>(IEnumerable<T>) будут совпадать, если вы ищете "Concat" с типом IEnumerable<> для типа строки. То, что действительно желательно, рекурсивно обрабатывает вложенные генерические типы, обрабатывая все общие параметры как соответствующие друг другу независимо от имени, а НЕ соответствующие конкретным типам.
  • Он возвращает первый метод, а не исключает исключение, если результат неоднозначен, например type.GetMethod(). Таким образом, вы можете получить метод, который вам нужен, если вам повезет, или вы не можете.
  • Иногда необходимо указать BindingFlags, чтобы избежать двусмысленности, например, когда метод производного класса "скрывает" метод базового класса. Обычно вы хотите найти методы базового класса, но не в специализированном случае, когда вы знаете, какой метод вы ищете, в производном классе. Или вы можете знать, что ищете статический метод экземпляра vs, public vs private и т.д. И не хотите, чтобы он соответствовал, если он не был точным.
  • Это не относится к другой серьезной ошибке с type.GetMethods(), поскольку она также не ищет базовые интерфейсы для методов при поиске метода по типу интерфейса. Хорошо, может быть, это придирчиво, но это еще одна серьезная ошибка в GetMethods(), которая для меня была проблемой.
  • Вызов type.GetMethods() неэффективен, type.GetMember(name, MemberTypes.Method, ...) будет возвращать только методы с совпадающим именем вместо всех методов в типе.
  • В качестве окончательного nit-pick имя 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" и типы массивов (refs игнорируются для сопоставления, но размеры массивов должны совпадать).

Итак, как Microsoft должен сделать это. Это действительно не так сложно.

Да, я знаю, вы можете сократить некоторые из этой логики с помощью Linq, но я не являюсь большим поклонником Linq в низкоуровневых подпрограммах, подобных этому, и также, если Linq не будет так же легко следовать, как исходный код, который часто не имеет места, IMO.

Если вы любите 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), который должен пройти для тип параметра (любой будет делать из-за соответствия подстановочных знаков). Однако вы не можете просто сделать new Type() - вам нужно найти реальный (или создать один с TypeBuilder). Чтобы сделать это проще, я добавил объявление public class T и добавил логику в IsSimilarType(), чтобы проверить его и сопоставить любой общий параметр. Если вам нужен T[], просто используйте T.MakeArrayType(1).

Ответ 2

К сожалению, дженерики не очень хорошо поддерживаются в .NET Reflection. В этом конкретном случае вам нужно вызвать 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<,>) });