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

Выбрать правильный общий метод с отражением

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

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

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

Однако проблема возникает, когда возникают разные общие перегрузки метода. Например, статические методы в классе System.Linq.Queryable. Существует два определения "Where'-method"

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?

4b9b3361

Ответ 1

Это можно сделать, но это не очень!

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

Ответ 2

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

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

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

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

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

Добавление

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

В С# отсутствует метод Clone, поэтому у нас есть свой. Он может принимать несколько аргументов, включая те, которые объясняют, как рекурсивно копировать свойства IEnumerable внутри исходного объекта.

Метод, который копирует IEnumerable, называется CopyList и выглядит следующим образом:

public static IEnumerable<TTo> CopyList<TTo>(
    IEnumerable<object> from,
    Func<PropertyInfo, bool> whereProps,
    Dictionary<Type, Type> typeMap
)
    where TTo : new()
{

Чтобы усложнить вещи (и согнуть мускулы этого подхода), у него есть несколько перегрузок, как этот:

public static IEnumerable<TTo> CopyList<TTo>(
    IEnumerable<object> from,
    Dictionary<Type, Type> typeMap
)
    where TTo : new()
{

Итак, у нас есть несколько (я покажу вам 2, но их в коде больше) сигнатур методов. У них одинаковое количество универсальных аргументов, но разное количество аргументов метода. Имена идентичны. Как мы можем вызвать правильный метод? Начните ниндзя С#!

var listTo = ReflectionHelper.GetIEnumerableType(
    fromValue.GetType());

var fn = new Func<
    IEnumerable<object>,
    Func<PropertyInfo, bool>,
    Dictionary<Type, Type>,
    IEnumerable<object>>(
        ModelTransform.CopyList<object>);

var copyListMethod = fn.GetMethodInfo()
    .GetGenericMethodDefinition()
    .MakeGenericMethod(listTo);

copyListMethod.Invoke(null,
    new object[] { fromValue, whereProps, typeMap });

В первой строке используется вспомогательный метод, к которому мы еще вернемся, но все, что он делает, - это получает общий тип списка IEnumerable в этом свойстве и присваивает его listTo. Следующая строка - это то место, где мы действительно приступаем к выполнению этого трюка, где мы выкладываем Func с адекватными параметрами, соответствующими конкретной перегрузке CopyList(), которую мы собираемся получить. В частности, желаемый CopyList() имеет 3 аргумента и возвращает IEnumerable<TTo>. Помните, что Func принимает свой возвращаемый тип в качестве последнего универсального аргумента, и что мы подставляем object везде, где есть универсальный в методе, который мы собираемся получить. Но, как вы можете видеть в этом примере, нам не нужно заменять объект где-либо еще. Например, мы знаем, что хотим передать предложение where, которое принимает PropertyInfo и возвращает true/false (bool), и мы просто говорим эти типы прямо в Func.

В качестве аргумента конструктора для функции Func мы передаем CopyList(), но помните, что имя CopyList расплывчато из-за перегрузок метода. Что действительно здорово, так это то, что С# сейчас выполняет тяжелую работу за вас, просматривая аргументы Func и выявляя нужные. На самом деле, если вы неправильно указали типы или количество аргументов, Visual Studio на самом деле пометит строку с ошибкой:

Никакая перегрузка для 'CopyList' не соответствует делегату 'Func...'

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

В третьей строке мы называем С# встроенным .GetMethodInfo(), а затем .MakeGeneric(listTo). Для этого у нас есть только один Generic, поэтому мы передаем его как listTo. Если бы у нас было 2, мы бы передали 2 аргумента здесь. Эти аргументы Type заменяют замены object, которые мы сделали ранее.

И что это - мы можем назвать copyListMethod(), без всяких строк, полностью безопасным для компиляции. Последняя строка делает вызов, сначала пропуская null, потому что это статический метод, затем массив object[] с 3 аргументами. Готово.

Я сказал, что вернусь к методу ReflectionHelper. Вот оно:

public static Type GetIEnumerableType(Type type)
{
    var ienumerable = type.GetInterface(typeof(System.Collections.Generic.IEnumerable<>).FullName);
    var generics = ienumerable.GetGenericArguments();
    return generics[0];
}

Ответ 3

Этот вопрос составляет около 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 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

Другое решение, которое может показаться вам полезным - можно получить 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)).

На самом деле, я использовал этот подход в plugin для .NET Reflection API. Вы можете проверить, как это работает здесь

Ответ 5

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

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

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

Ответ 7

В дополнение к ответу @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));  

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

Ответ Криса Москини хорош, когда вы знаете имя метода во время компиляции. Ответ Antamir работает, если мы получим имя метода во время выполнения, но довольно перебор.

Я использую другой способ, для которого я получил вдохновение с помощью рефлектора из .NET-функции Expression.Call, которая выбирает правильный общий метод из строки.

public static MethodInfo GetGenericMethod(Type declaringType, string methodName, Type[] typeArgs, params Type[] argTypes) {
    foreach (var m in from m in declaringType.GetMethods()
                        where m.Name == methodName
                            && typeArgs.Length == m.GetGenericArguments().Length
                            && argTypes.Length == m.GetParameters().Length
                        select m.MakeGenericMethod(typeArgs)) {
        if (m.GetParameters().Select((p, i) => p.ParameterType == argTypes[i]).All(x => x == true))
            return m;
    }

    return null;
}

Использование:

var m = ReflectionUtils.GetGenericMethod(typeof(Queryable), "Where", new[] { typeof(Person) }, typeof(IQueryable<Person>), typeof(Expression<Func<Person, bool>>));

Если вам нужно только определение общего метода или просто не знаю тип T в то время, вы можете использовать некоторые фиктивные типы, а затем линять общую информацию:

var m = ReflectionUtils.GetGenericMethod(typeof(Queryable), "Where", new[] { typeof(object) }, typeof(IQueryable<object>), typeof(Expression<Func<object, bool>>));
m = m.GetGenericMethodDefinition();

Ответ 11

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

Например, если вы запустили:

type.GetMethod("MyMethod",typeof(Refl.T1),typeof(bool))

он не может различать два метода:

MyMethod<T>(T arg1)
MyMethod<T>(T arg1, bool arg2)

Два вызова:

var p = method.GetParameters();   

следует изменить на:

var p = method.GetParameters();   
if (p.Length != parameters.Length)
{
    correct = false;
    continue;
}

Кроме того, обе существующие строки "break" должны быть "continue".

Ответ 12

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

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

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

carList.FirstOrDefault(e => e.GetType().GetProperty(attributeName).GetValue(e, null) as string== attributeValue);