2010-10-27 18 views
43

estoy tratando de recuperar MethodInfo para Where método del tipo Enumerable:GetMethod método genérico para

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

pero llegar nula. ¿Qué estoy haciendo mal?

+1

Obtener el tipo de Func me parece el obstáculo. –

+0

Otro duplicado exacto: [select-right-generic-method-with-reflection] (http://stackoverflow.com/questions/3631547/select-right-generic-method-with-reflection) – nawfal

Respuesta

42

Esa respuesta anterior funciona para algunos casos, sin embargo:

  • No maneja los tipos genéricos anidados, tales como un tipo de parámetro de Action<IEnumerable<T>>. Se tratarán todos los Action<> como coincidencias, por ejemplo, string.Concat(IEnumerable<string>) y string.Concat<T>(IEnumerable<T>) coincidirán si se busca "Concat" con el tipo IEnumerable<> en el tipo de cadena. Lo que es realmente deseable es manejar los tipos genéricos anidados de manera recursiva, mientras se tratan todos los parámetros genéricos para que coincidan entre sí, independientemente del nombre, mientras que NO coinciden con los tipos concretos.
  • Devuelve el primer método coincidente en lugar de arrojar una excepción si el resultado es ambiguo, como type.GetMethod(). Por lo tanto, puede obtener el método que quería si tiene suerte, o no.
  • A veces será necesario especificar BindingFlags para evitar la ambigüedad, como cuando un método de clase derivado 'oculta' un método de clase base. Normalmente desea buscar métodos de clase base, pero no en un caso especializado donde sabe que el método que está buscando está en la clase derivada. O bien, es posible que sepa que está buscando un método estático vs instancia, público vs privado, etc. y no desea que coincida si no es exacto.
  • No soluciona otro error importante con type.GetMethods(), ya que tampoco busca interfaces de base para métodos cuando busca un método en un tipo de interfaz. OK, tal vez sea exigente, pero es otro gran error en GetMethods() que ha sido un problema para mí.
  • Llamar a type.GetMethods() es ineficiente, type.GetMember(name, MemberTypes.Method, ...) devolverá solo los métodos con un nombre coincidente en lugar de TODOS los métodos en el tipo.
  • Como una nit-pick final, el nombre GetGenericMethod() podría ser engañoso, ya que podría estar tratando de encontrar un método no genérico que tenga un parámetro de tipo en algún tipo de parámetro debido a un tipo genérico de declaración.

Aquí hay una versión que trata todas estas cosas, y se puede utilizar como un reemplazo de uso general para el defectuoso GetMethod(). Tenga en cuenta que se proporcionan dos métodos de extensión, uno con BindingFlags y otro sin (por conveniencia).

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

Tenga en cuenta que el método IsSimilarType(Type) extensión puede hacerse pública y podría ser útil por sí misma. Lo sé, el nombre no es muy bueno, puedes inventar uno mejor, pero podría ser realmente largo explicar lo que hace. Además, agregue otra mejora comprobando para 'ref' y tipos de matriz (las referencias se ignoran para la coincidencia, pero las dimensiones de las matrices deben coincidir).

Así, así es como Microsoft debería hacerlo. Realmente no es tan difícil.

Sí, lo sé, puedes acortar algo de esa lógica usando Linq, pero no soy un gran admirador de Linq en rutinas de bajo nivel como este, y tampoco a menos que el Linq sea tan fácil de seguir como el código original, que a menudo NO es el caso, IMO.

Si te gusta LINQ, y se debe, se puede reemplazar la parte más interna de IsSimilarType() con esto (8 vueltas en las líneas 1):

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

Una última cosa: Si usted está buscando un método genérico con un parámetro genérico, como Method<T>(T, T[]), deberá encontrar un tipo que sea un parámetro genérico (IsGenericParameter == true) para pasar el tipo de parámetro (cualquiera lo hará, debido a la coincidencia de 'comodín'). Sin embargo, no puede simplemente hacer new Type() - tiene que encontrar uno real (o compilar uno con TypeBuilder). Para hacerlo más fácil, agregué la declaración public class T, y agregué lógica al IsSimilarType() para verificarla y hacer coincidir cualquier parámetro genérico. Si necesita un T[], simplemente use T.MakeArrayType(1).

+0

"Entonces, así es como Microsoft debería haberlo hecho. Realmente no es tan difícil". Sí, excepto que tu código aún no funciona. Si el argumento es System.Type y el valor pasado es System.RuntimeType no funciona. –

+0

Cameron, creo que probablemente estés haciendo algo mal por tener ese problema. ¿Tal vez llamaste a GetType() en un objeto Type? Ver la respuesta en esta publicación: http://stackoverflow.com/questions/5737840/whats-the-difference-between-system-type-and-system-runtimetype-in-c –

+0

@Ken_Beckett No, solo quiero llamar un método con la firma 'SomeMethod (Type t)' yendo 'dynamicInstance.SomeMethod (other.GetType())'. El problema es 'IsSimilarType' devuelve falso para' System.RuntimeType' y 'System.Type'. Al solucionar ese problema, tuve problemas para llamar a métodos con parámetros genéricos. Todo el código no funciona en todos los casos, así que diría que el problema ** es ** tan difícil. –

22

Desafortunadamente, los genéricos no son bien compatibles en .NET Reflection. En este caso particular, deberá llamar a GetMethods y luego filtrar el conjunto de resultados para el método que está buscando. Un método de extensión como el siguiente debería ser el truco.

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

Con esto en la mano el siguiente código funcionará:

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

O en una línea, devuelva 'type.GetMethods(). FirstOrDefault (m => m.Name == nombre && m.GetParameters(). Seleccione (p => p.ParameterType) .SequenceEqual (parameterTypes, new SimpleTypeComparer()));' :) Adding 'params' al último parámetro sería más agradable. – nawfal

Cuestiones relacionadas