Выберите правильный общий метод с отражением - PullRequest
32 голосов
/ 03 сентября 2010

Я хочу выбрать правильный обобщенный метод с помощью отражения и затем вызвать его.

Обычно это довольно просто.Например,

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

Однако проблема возникает, когда существуют различные общие перегрузки метода.Например, статические методы в классе System.Linq.Queryable.Существует два определения метода «Где»

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

Однако я не доволен этим, потому что я делаю огромное предположение, что первый метод - правильный.Я скорее хочу найти правильный метод по типу аргумента.Но я не мог понять, как.

Я попробовал это с передачей 'типов', но это не сработало.

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

Так что кто-нибудь знает, как мне найти«правильный» универсальный метод через рефлексию.Например, правильная версия метода Where в Queryable-классе?

Ответы [ 12 ]

40 голосов
/ 23 марта 2013

Вы можете несколько элегантно выбрать конкретную универсальную перегрузку метода во время компиляции, не передавая строки для поиска во время выполнения, как это делают другие ответы.

Статические методы

Предположим, у вас есть несколько статических методов с одним именем, например:

public static void DoSomething<TModel>(TModel model)

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

// etc

Если вы создаете Action или Func, который соответствует общему количеству и количеству параметров искомой перегрузки, вы можете выбрать его во время компиляции с относительно небольшим количеством акробатики.

Пример: выберите первый метод - возвращает void, поэтому используйте Action, требуется один универсальный. Мы используем объект, чтобы пока не указывать тип:

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

Пример: выберите второй метод - возвращает void, поэтому Action, 2 универсальных типа, поэтому используйте тип объекта дважды, один раз для каждого из 2 универсальных параметров:

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

Вы только что получили метод, который вам нужен, без каких-либо сумасшедших действий по сантехнике и без поиска во время выполнения или использования рискованных строк.

MethodInfo

Обычно в Reflection вам нужен объект MethodInfo, который вы также можете получить безопасным для компиляции способом. Это когда вы передаете фактические универсальные типы, которые вы хотите использовать в своем методе. Предполагая, что вы хотели второй метод выше:

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

Это ваш универсальный метод, без поиска отражений, вызовов GetMethod () или хлипких строк.

Методы статического расширения

Конкретный пример, который вы приводите в Queryable. Перегрузки где заставляют вас немного задуматься в определении 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);

Методы экземпляра

Включение комментария Валери - чтобы получить метод экземпляра, вам нужно сделать что-то очень похожее. Предположим, у вас есть этот метод экземпляра в вашем классе:

public void MyMethod<T1>(T1 thing)

Сначала выберите метод так же, как для статики:

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

Затем вызовите GetGenericMethodDefinition(), чтобы перейти к универсальному MethodInfo, и, наконец, передайте ваш тип (ы) с помощью MakeGenericMethod():

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

Разделение MethodInfo и типов параметров

Это не было запрошено в вопросе, но как только вы сделаете вышеописанное, вы можете выбрать метод в одном месте и решить, какие типы передать в другом. Вы можете отделить эти 2 шага.

Если вы не уверены в параметрах универсального типа, которые собираетесь передать, вы всегда можете получить объект MethodInfo без них.

Статический:

var methodInfo = method.Method;

Instance:

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

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

Правки: Исправлены объяснения, включен пример метода экземпляра Валери.

22 голосов
/ 03 сентября 2010

Это можно сделать, но это не красиво!

Например, чтобы получить первую перегрузку 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();
18 голосов
/ 29 февраля 2012

Этому вопросу около 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" (статический класс, который содержит все мои различные функции расширения утилит Reflection и т. Д. Они служат в качестве заполнители для того места, где обычно должны располагаться параметры типа. Приведенные выше примеры соответствуют получению следующих методов 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;
}

Приведенный выше код выполняет основную часть работы - он перебирает все методы определенного типа и сравнивает их с заданными типами параметров для поиска. Но ждать! Как насчет этой «замещающей» функции? Это хорошая маленькая рекурсивная функция, которая будет искать по всему дереву типов параметров - в конце концов, тип параметра сам может быть универсальным типом, который может содержать 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;
}
4 голосов
/ 24 сентября 2014

Еще одно решение, которое может оказаться полезным - можно получить MethodInfo на основе Expression.Call, в котором уже есть логика для разрешения перегрузки.

Например, если вам нужно получить какой-то конкретный метод 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 голосов
/ 05 июня 2014

Пусть компилятор сделает это за вас:

var fakeExp = (Expression<Func<IQueryable<int>, IQueryable<int>>>)(q => q.Where((x, idx) => x> 2));
var mi = ((MethodCallExpression)fakeExp.Body).Method.GetGenericMethodDefinition();

для Where с индексом или просто пропустите второй параметр в выражении Where для параметра без

2 голосов
/ 01 марта 2015

В дополнение к ответу @ 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));  
2 голосов
/ 03 сентября 2010

Использовать DynamicMethods.GenericMethodInvokerMethod , GetMethod недостаточно для использования с генериками

1 голос
/ 23 марта 2019

У меня похожая проблема, и я подумал, что опубликую свое решение здесь. Я пытаюсь вызвать несколько функций:

p.Foo<Klass1>(true)
p.Foo<Klass2>(true)
p.Foo<Klass3>(true)
bool k1 = p.Bar<Klass1>()
bool k2 = p.Bar<Klass2>()
bool k3 = p.Bar<Klass3>()

Мое решение:

public static TAction RemapGenericMember<TAction>(object parent, Type target, TAction func) where TAction : Delegate { 
    var genericMethod = func?.Method?.GetGenericMethodDefinition()?.MakeGenericMethod(target);
    if (genericMethod.IsNull()) {
        throw new Exception($"Failed to build generic call for '{func.Method.Name}' with generic type '{target.Name}' for parent '{parent.GetType()}'");
    }
    return (TAction)genericMethod.CreateDelegate(typeof(TAction), parent);
}

А теперь я могу позвонить:

foreach(var type in supportedTypes) {
   InvokeGenericMember<Action<bool>>(p, type, Foo<object>)(true);
   bool x = InvokeGenericMember<Function<bool>>(p, type, Bar<object>)();
}
1 голос
/ 11 мая 2016

Я сделал небольшой вспомогательный функционал:

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)) 
           });
0 голосов
/ 20 января 2016

Я нашел самый простой способ использования выражений iQuerable при вызове метода с использованием отражения.Пожалуйста, смотрите код ниже:

Вы можете использовать выражение IQuerable согласно требованию.

var attributeName = "CarName";
var attributeValue = "Honda Accord";

carList.FirstOrDefault(e => e.GetType().GetProperty(attributeName).GetValue(e, null) as string== attributeValue);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...