web-dev-qa-db-fra.com

GetMethod pour la méthode générique

J'essaie de récupérer MethodInfo pour la méthode Where de type Enumerable:

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

mais obtenez null. Qu'est-ce que je fais mal?

47
SiberianGuy

Cette réponse précédente fonctionne cependant dans certains cas:

  • Il ne gère pas les types génériques imbriqués, comme un type de paramètre de Action<IEnumerable<T>>. Il traitera tous les Action<> Comme des correspondances, par exemple, string.Concat(IEnumerable<string>) et string.Concat<T>(IEnumerable<T>) correspondront tous les deux si vous recherchez "Concat" Avec le type IEnumerable<> sur le type de chaîne. Ce qui est vraiment souhaitable, c'est de gérer les types génériques imbriqués de manière récursive, tout en traitant tous les paramètres génériques comme correspondant les uns aux autres, quel que soit leur nom, tout en ne correspondant PAS aux types concrets.
  • Il renvoie la première méthode correspondante plutôt que de lever une exception si le résultat est ambigu, comme le fait type.GetMethod(). Donc, vous pourriez obtenir la méthode que vous vouliez si vous avez de la chance, ou vous pourriez ne pas.
  • Parfois, il sera nécessaire de spécifier BindingFlags afin d'éviter toute ambiguïté, comme lorsqu'une méthode de classe dérivée "cache" une méthode de classe de base. Vous voulez normalement trouver des méthodes de classe de base, mais pas dans un cas spécialisé où vous savez que la méthode que vous recherchez se trouve dans la classe dérivée. Ou, vous savez peut-être que vous recherchez une méthode statique vs instance, publique vs privée, etc. et que vous ne voulez pas correspondre si elle n'est pas exacte.
  • Il ne corrige pas un autre défaut majeur avec type.GetMethods(), en ce qu'il ne recherche pas non plus les interfaces de base pour les méthodes lors de la recherche d'une méthode sur un type d'interface. OK, c'est peut-être difficile, mais c'est une autre faille majeure dans GetMethods() qui a été un problème pour moi.
  • Appeler type.GetMethods() est inefficace, type.GetMember(name, MemberTypes.Method, ...) renverra uniquement les méthodes avec un nom correspondant au lieu de TOUTES les méthodes du type.
  • En dernier recours, le nom GetGenericMethod() pourrait être trompeur, car vous pourriez essayer de trouver une méthode non générique qui se trouve avoir un paramètre de type quelque part dans un type de paramètre en raison d'un type déclarant générique .

Voici une version qui traite de toutes ces choses, et peut être utilisée comme un remplacement général pour la GetMethod() défectueuse. Notez que deux méthodes d'extension sont fournies, une avec BindingFlags et une sans (pour plus de commodité).

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

Notez que la méthode d'extension IsSimilarType(Type) peut être rendue publique et peut être utile seule. Je sais, le nom n'est pas génial - vous pouvez en proposer un meilleur, mais il pourrait être très long d'expliquer ce qu'il fait. J'ai également ajouté une autre amélioration en vérifiant les types de référence et de tableau (les références sont ignorées pour la correspondance, mais les dimensions des tableaux doivent correspondre).

C'est ainsi que Microsoft devrait l'a fait. Ce n'est vraiment pas si difficile.

Oui, je sais, vous pouvez raccourcir une partie de cette logique en utilisant Linq, mais je ne suis pas un grand fan de Linq dans des routines de bas niveau comme celle-ci, et pas à moins que le Linq soit à peu près aussi facile à suivre que le code d'origine, ce qui n'est souvent pas le cas, OMI.

Si vous aimez Linq, et vous devez, vous pouvez remplacer la partie la plus interne de IsSimilarType() par ceci (transforme 8 lignes en 1):

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

Une dernière chose: si vous recherchez une méthode générique avec un paramètre générique, comme Method<T>(T, T[]), vous devrez trouver un type qui est un paramètre générique (IsGenericParameter == true) Pour passez pour le type de paramètre (tout le monde le fera, en raison de la correspondance "générique"). Cependant, vous ne pouvez pas simplement faire new Type() - vous devez en trouver un réel (ou en construire un avec TypeBuilder). Pour rendre cela plus facile, j'ai ajouté la déclaration public class T Et ajouté de la logique à IsSimilarType() pour la vérifier et faire correspondre tout paramètre générique. Si vous avez besoin d'un T[], Utilisez simplement T.MakeArrayType(1).

49
Ken Beckett

Malheureusement, les génériques ne sont pas bien pris en charge dans .NET Reflection. Dans ce cas particulier, vous devrez appeler GetMethods, puis filtrer le jeu de résultats pour la méthode que vous recherchez. Une méthode d'extension comme la suivante devrait faire l'affaire.

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

Avec cela en main, le code suivant fonctionnera:

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