Как использовать рефлексию для вызова универсального метода?

911

Какой лучший способ вызвать общий метод, когда параметр типа неизвестен во время компиляции, но вместо этого получается динамически во время выполнения?

Рассмотрим следующий пример кода - внутри метода Example(), какой наиболее сжатый способ вызвать GenericMethod<T>() с помощью Type, сохраненного в переменной myType?

public class Sample
{
    public void Example(string typeName)
    {
        Type myType = FindType(typeName);

        // What goes here to call GenericMethod<T>()?
        GenericMethod<myType>(); // This doesn't work

        // What changes to call StaticMethod<T>()?
        Sample.StaticMethod<myType>(); // This also doesn't work
    }

    public void GenericMethod<T>()
    {
        // ...
    }

    public static void StaticMethod<T>()
    {
        //...
    }
}
  • 6
    Я попробовал решение Джона и не смог заставить его работать, пока я не обнародовал универсальный метод в своем классе. Я знаю, что другой Джон ответил, что вам нужно указать флаги привязки, но это не помогло.
  • 10
    Вам также нужен BindingFlags.Instance , а не только BindingFlags.NonPublic , чтобы получить метод private / internal.
Показать ещё 2 комментария
Теги:
generics
reflection

8 ответов

901
Лучший ответ

Вам нужно использовать отражение, чтобы начать использовать метод, а затем "построить" его, предоставив аргументы типа MakeGenericMethod:

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Для статического метода передайте null в качестве первого аргумента Invoke. Это не имеет ничего общего с универсальными методами - это просто нормальное отражение.

Как уже отмечалось, многие из них проще с С# 4 с использованием dynamic - если вы можете использовать вывод типа, конечно. Это не помогает в случаях, когда вывод типа недоступен, например, точный пример в вопросе.

  • 82
    +1; обратите внимание, что GetMethod() рассматривает только общедоступные методы экземпляра по умолчанию, поэтому вам могут понадобиться BindingFlags.Static и / или BindingFlags.NonPublic .
  • 13
    Правильная комбинация флагов - BindingFlags.NonPublic | BindingFlags.Instance (и, необязательно, BindingFlags.Static ).
Показать ещё 17 комментариев
149

Просто добавление к исходному ответу. Хотя это будет работать:

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Также немного опасно, что вы теряете проверку времени компиляции для GenericMethod. Если вы позже сделаете рефакторинг и переименуете GenericMethod, этот код не заметит и не будет работать во время выполнения. Кроме того, если есть какая-либо пост-обработка сборки (например, обфускация или удаление неиспользуемых методов/классов), этот код также может сломаться.

Итак, если вы знаете метод, с которым вы связываетесь во время компиляции, и это не называется миллионы раз, поэтому накладные расходы не имеют значения, я бы изменил этот код:

Action<> GenMethod = GenericMethod<int>;  //change int by any base type 
                                          //accepted by GenericMethod
MethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

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

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

  • 5
    В случаях, когда для вызова метода используется отражение, обычно имя метода само обнаруживается другим методом. Знание имени метода заранее не распространено.
  • 11
    Ну, я согласен на общее использование отражения. Но первоначальный вопрос заключался в том, как вызвать «GenericMethod <myType> ()». Если бы этот синтаксис был разрешен, нам бы вообще не понадобился GetMethod (). Но на вопрос «как мне написать« GenericMethod <myType> »? Я думаю, что ответ должен включать способ избежать потери связи во время компиляции с GenericMethod. Теперь, если этот вопрос является распространенным или нет, я не знаю, но Я точно знаю, что у меня была именно эта проблема вчера, и именно поэтому я попал в этот вопрос.
Показать ещё 5 комментариев
113

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

Чтобы использовать этот метод, тип должен быть известен из фактического объекта (а не только для экземпляра класса Type). В противном случае вам нужно создать объект такого типа или использовать стандартный API описания отражения . Вы можете создать объект, используя метод dynamic. Вот пример:

class Alpha { }
class Beta { }
class Service
{
    public void Process<T>(T item)
    {
        Console.WriteLine("item.GetType(): " + item.GetType()
                          + "\ttypeof(T): " + typeof(T));
    }
}

class Program
{
    static void Main(string[] args)
    {
        var a = new Alpha();
        var b = new Beta();

        var service = new Service();
        service.Process(a); // Same as "service.Process<Alpha>(a)"
        service.Process(b); // Same as "service.Process<Beta>(b)"

        var objects = new object[] { a, b };
        foreach (var o in objects)
        {
            service.Process(o); // Same as "service.Process<object>(o)"
        }
        foreach (var o in objects)
        {
            dynamic dynObj = o;
            service.Process(dynObj); // Or write "service.Process((dynamic)o)"
        }
    }
}

И вот вывод этой программы:

item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta
item.GetType(): Alpha    typeof(T): System.Object
item.GetType(): Beta     typeof(T): System.Object
item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta

Process - это общий метод экземпляра, который записывает реальный тип переданного аргумента (с использованием метода GetType()) и тип общего параметра (с помощью оператора typeof).

Отбрасывая аргумент object в тип dynamic, мы откладываем предоставление параметра типа до выполнения. Когда метод Process вызывается с аргументом dynamic, тогда компилятор не заботится о типе этого аргумента. Компилятор генерирует код, который во время выполнения проверяет реальные типы переданных аргументов (используя отражение) и выбирает лучший метод для вызова. Здесь существует только один общий метод, поэтому он вызывается с правильным параметром типа.

В этом примере вывод такой же, как если бы вы написали:

foreach (var o in objects)
{
    MethodInfo method = typeof(Service).GetMethod("Process");
    MethodInfo generic = method.MakeGenericMethod(o.GetType());
    generic.Invoke(service, new object[] { o });
}

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

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

class Program
{
    static void Main(string[] args)
    {
        object obj = new Alpha();

        Helper((dynamic)obj);
    }

    public static void Helper<T>(T obj)
    {
        GenericMethod<T>();
    }

    public static void GenericMethod<T>()
    {
        Console.WriteLine("GenericMethod<" + typeof(T) + ">");
    }
}

Повышенная безопасность типов

Что действительно отличает нас от использования объекта dynamic в качестве замены для использования API отражения, так это то, что вы теряете только проверку времени компиляции этого типа, который вы не знаете до выполнения. Другие аргументы и имя метода статически анализируются компилятором, как обычно. Если вы удалите или добавите больше аргументов, измените их типы или переименуйте имя метода, вы получите ошибку времени компиляции. Это не произойдет, если вы укажете имя метода как строку в Type.GetMethod и аргументы как массив объектов в MethodInfo.Invoke.

Ниже приведен простой пример, иллюстрирующий, как некоторые ошибки могут быть пойманы во время компиляции (прокомментированный код) и другие во время выполнения. Он также показывает, как DLR пытается решить, какой метод вызывать.

interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }

class Program
{
    static void Main(string[] args)
    {
        var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
        for (int i = 0; i < objects.Length; i++)
        {
            ProcessItem((dynamic)objects[i], "test" + i, i);

            //ProcesItm((dynamic)objects[i], "test" + i, i);
            //compiler error: The name 'ProcesItm' does not
            //exist in the current context

            //ProcessItem((dynamic)objects[i], "test" + i);
            //error: No overload for method 'ProcessItem' takes 2 arguments
        }
    }

    static string ProcessItem<T>(T item, string text, int number)
        where T : IItem
    {
        Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
                          typeof(T), text, number);
        return "OK";
    }
    static void ProcessItem(BarItem item, string text, int number)
    {
        Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
    }
}

Здесь мы снова выполняем некоторый метод, отбрасывая аргумент в тип dynamic. Только проверка первого типа аргументов переносится на время выполнения. Вы получите ошибку компилятора, если имя метода, который вы вызываете, не существует или другие аргументы недействительны (неправильное количество аргументов или неправильных типов).

Когда вы передаете аргумент dynamic методу, то этот вызов в последнее время связан. Разрешение перегрузки метода происходит во время выполнения и пытается выбрать наилучшую перегрузку. Поэтому, если вы вызываете метод ProcessItem с объектом типа BarItem, то вы на самом деле вызываете не-общий метод, потому что он лучше подходит для этого типа. Однако вы получите ошибку времени выполнения, когда передаете аргумент типа Alpha, потому что нет метода, который может обрабатывать этот объект (общий метод имеет ограничение where T : IItem и Alpha class не реализует этот интерфейс). Но это все. У компилятора нет информации о том, что этот вызов действителен. Вы, как программист, знаете это, и вы должны убедиться, что этот код работает без ошибок.

Тип возврата getcha

Когда вы вызываете не-void-метод с параметром динамического типа, его возвращаемый тип, вероятно, be dynamic тоже. Поэтому, если вы изменили предыдущий пример на этот код:

var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

тогда тип объекта результата будет dynamic. Это связано с тем, что компилятор не всегда знает, какой метод будет вызываться. Если вам известен тип возвращаемого вызова функции, вы должны неявно преобразовать его в требуемый тип, чтобы остальная часть кода была статически введена:

string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

Вы получите ошибку времени выполнения, если тип не соответствует.

На самом деле, если вы попытаетесь получить значение результата в предыдущем примере, вы получите ошибку времени выполнения во второй итерации цикла. Это связано с тем, что вы пытались сохранить возвращаемое значение функции void.

  • 0
    Мариуш, сбит с толку: «Однако вы получите ошибку во время выполнения, когда передадите аргумент типа Alpha, потому что нет метода, который может обработать этот объект». Если я вызову var a = new Alpha () ProcessItem (a, «test» + i i) Почему бы универсальный метод ProcessItem не справился бы с этой задачей эффективно, выдав «Общий элемент процесса»?
  • 0
    @AlexEdelstein Я отредактировал свой ответ, чтобы немного уточнить. Это потому, что универсальный метод ProcessItem имеет общие ограничения и принимает только объект, который реализует интерфейс IItem . Когда вы будете вызывать ProcessItem(new Aplha(), "test" , 1); или ProcessItem((object)(new Aplha()), "test" , 1); вы получите ошибку компилятора, но при преобразовании в dynamic вы откладываете эту проверку до времени выполнения.
Показать ещё 1 комментарий
11

С С# 4.0 отражение не требуется, поскольку DLR может вызывать его с использованием типов времени выполнения. Поскольку использование библиотеки DLR - это своего рода боль динамически (вместо кода генерации компилятора С# для вас), среда с открытым исходным кодом Dynamitey (.net standard 1.5) дает вам простой кешированный доступ во время выполнения к тем же вызовам, которые компилятор будет генерировать для тебя.

var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));
10

Это то же самое, что вопрос, который я задал на другой неделе: Отражение и общие типы

Затем я рассмотрел, как вызвать общий перегруженный метод в моем блоге: http://www.aaron-powell.com/reflection-and-generics

6

Добавляем к ответ Адриана Галлеро:

Вызов универсального метода из типа info включает в себя три шага.

TL;DR: вызов известного общего метода с объектом типа может выполняться с помощью:

((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition()
    .MakeGenericMethod(typeof(string))
    .Invoke(this, null);

где GenericMethod<object> - имя метода для вызова и любой тип, удовлетворяющий общим ограничениям.

(Действие) соответствует сигнатуре метода, который будет называться i.e. (Func<string,string,int> или Action<bool>)

Шаг 1 получает MethodInfo для определения общего метода

Способ 1. Используйте GetMethod() или GetMethods() с соответствующими типами или флагами привязки.

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");

Способ 2. Создайте делегат, получите объект MethodInfo и затем вызовите GetGenericMethodDefinition

Изнутри класса, содержащего методы:

MethodInfo method = ((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

Извне класса, содержащего методы:

MethodInfo method = ((Action)(new Sample())
    .GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)Sample.StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

В С# имя метода, то есть "ToString" или "GenericMethod", фактически относится к группе методов, которые могут содержать один или несколько методов. Пока вы не укажете типы параметров метода, неизвестно, какие метод, на который вы ссылаетесь.

((Action)GenericMethod<object>) относится к делегату для конкретного метода. ((Func<string, int>)GenericMethod<object>) относится к другой перегрузке GenericMethod

Способ 3. Создайте выражение лямбда, содержащее выражение вызова метода, получите объект MethodInfo, а затем GetGenericMethodDefinition

MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
    (Sample v) => v.GenericMethod<object>()
    )).Body).Method.GetGenericMethodDefinition();

Это разбивается на

Создайте выражение лямбда, где тело является вызовом вашего желаемого метода.

Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();

Извлеките тело и добавьте в MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

Получить определение общего метода из метода

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

Шаг 2 вызывает метод MakeGenericMethod для создания общего метода с соответствующими типами.

MethodInfo generic = method.MakeGenericMethod(myType);

Шаг 3 вызывает метод с соответствующими аргументами.

generic.Invoke(this, null);
2

Никто не предоставил "классическое отражение", поэтому здесь приведен полный пример кода:

using System;
using System.Collections;
using System.Collections.Generic;

namespace DictionaryRuntime
{
    public class DynamicDictionaryFactory
    {
        /// <summary>
        /// Factory to create dynamically a generic Dictionary.
        /// </summary>
        public IDictionary CreateDynamicGenericInstance(Type keyType, Type valueType)
        {
            //Creating the Dictionary.
            Type typeDict = typeof(Dictionary<,>);

            //Creating KeyValue Type for Dictionary.
            Type[] typeArgs = { keyType, valueType };

            //Passing the Type and create Dictionary Type.
            Type genericType = typeDict.MakeGenericType(typeArgs);

            //Creating Instance for Dictionary<K,T>.
            IDictionary d = Activator.CreateInstance(genericType) as IDictionary;

            return d;

        }
    }
}

Вышеупомянутый класс DynamicDictionaryFactory имеет метод

CreateDynamicGenericInstance(Type keyType, Type valueType)

и он создает и возвращает экземпляр IDictionary, типы ключей и значений которых точно указаны в вызове keyType и valueType.

Вот полный пример, как вызвать этот метод для создания экземпляра и использования Dictionary<String, int>:

using System;
using System.Collections.Generic;

namespace DynamicDictionary
{
    class Test
    {
        static void Main(string[] args)
        {
            var factory = new DictionaryRuntime.DynamicDictionaryFactory();
            var dict = factory.CreateDynamicGenericInstance(typeof(String), typeof(int));

            var typedDict = dict as Dictionary<String, int>;

            if (typedDict != null)
            {
                Console.WriteLine("Dictionary<String, int>");

                typedDict.Add("One", 1);
                typedDict.Add("Two", 2);
                typedDict.Add("Three", 3);

                foreach(var kvp in typedDict)
                {
                    Console.WriteLine("\"" + kvp.Key + "\": " + kvp.Value);
                }
            }
            else
                Console.WriteLine("null");
        }
    }
}

Когда выполняется вышеуказанное консольное приложение, мы получаем правильный ожидаемый результат:

Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3
0

Это мои 2 цента на основе Grax answer, но с двумя параметрами, необходимыми для общего метода.

Предположим, что ваш метод определяется в классе Helpers следующим образом:

public class Helpers
{
    public static U ConvertCsvDataToCollection<U, T>(string csvData)
    where U : ObservableCollection<T>
    {
      //transform code here
    }
}

В моем случае тип U всегда является наблюдаемым объектом хранения коллекции типа T.

Поскольку у меня есть предопределенные типы, я сначала создаю объекты "dummy", которые представляют наблюдаемую коллекцию (U) и объект, хранящийся в ней (T), и которые будут использоваться ниже, чтобы получить их тип при вызове Make

object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);

Затем вызовите GetMethod, чтобы найти свою общую функцию:

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

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

Вам нужно передать массив Type [] в функцию MakeGenericMethod, которая содержит типы объектов "dummy", которые были созданы выше:

MethodInfo generic = method.MakeGenericMethod(
new Type[] {
   myCollection.GetType(),
   myObject.GetType()
});

После этого вам нужно вызвать метод Invoke, как указано выше.

generic.Invoke(null, new object[] { csvData });

И все готово. Работает в обаянии!

UPDATE:

Как подчеркнул @Bevan, мне не нужно создавать массив при вызове функции MakeGenericMethod, поскольку он принимает параметры, и мне не нужно создавать объект, чтобы получить типы, поскольку я могу просто передать типы напрямую эта функция. В моем случае, поскольку у меня есть типы, предопределенные в другом классе, я просто изменил свой код на:

object myCollection = null;

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

MethodInfo generic = method.MakeGenericMethod(
   myClassInfo.CollectionType,
   myClassInfo.ObjectType
);

myCollection = generic.Invoke(null, new object[] { csvData });

myClassInfo содержит 2 свойства типа Type, которые я установил во время выполнения на основе значения перечисления, переданного конструктору, и предоставит мне соответствующие типы, которые затем я использую в MakeGenericMethod.

Еще раз спасибо за выделение этого @Bevan.

  • 0
    Аргументы для MakeGenericMethod() имеют ключевое слово params, поэтому вам не нужно создавать массив; вам также не нужно создавать экземпляры для получения типов - будет достаточно methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject)) .

Ещё вопросы

Сообщество Overcoder
Наверх
Меню