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

1071

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

Рассмотрим следующий пример кода - внутри 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>()
    {
        //...
    }
}
Беван
источник
7
Я попробовал решение Джона и не смог заставить его работать, пока я не обнародовал универсальный метод в своем классе. Я знаю, что другой Джон ответил, что вам нужно указать флаги привязки, но это не помогло.
Наскев
12
Вам также нужно BindingFlags.Instanceне просто BindingFlags.NonPublicполучить метод private / internal.
Ларс Кемманн
2
Современная версия этого вопроса: stackoverflow.com/q/2433436/103167
Бен Фойгт
@ Питер Мортенсен - кстати, я использовал пробелы перед '?' отделить английские части от неанглийских (C #) частей; ИМХО удаление пробела делает его похожим на? является частью кода. Если бы не было кода, я бы, конечно, согласился с удалением пробелов, но в этом случае ...
Беван

Ответы:

1139

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

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

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

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

Джон Скит
источник
92
+1; обратите внимание, что GetMethod()по умолчанию рассматриваются только общедоступные методы экземпляров, поэтому вам может понадобиться BindingFlags.Staticи / или BindingFlags.NonPublic.
20
Правильная комбинация флагов есть BindingFlags.NonPublic | BindingFlags.Instance(и опционально BindingFlags.Static).
Ларс Кемманн
4
Вопрос, отмеченный как дублирование этого вопроса, задается вопросом, как сделать это с помощью статических методов - и технически так же возникает вопрос здесь. Первый параметр generic.Invoke () должен быть нулевым при вызове статических методов. Первый параметр необходим только при вызове методов экземпляра.
Крис Москини
2
@ChrisMoschini: добавил это к ответу.
Джон Скит
2
@gzou: Я добавил что - то в ответ , - но обратите внимание , что для вызова общих методов в вопросе , dynamicне помогает , потому что умозаключение типа не доступно. (Нет аргументов, которые компилятор может использовать для определения аргумента типа.)
Джон Скит
170

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

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
В случаях, когда для вызова метода используется отражение, обычно имя метода само обнаруживается другим методом. Знание имени метода заранее не распространено.
Беван
13
Ну, я согласен на общее использование отражения. Но первоначальный вопрос заключался в том, как вызвать «GenericMethod <myType> ()». Если бы этот синтаксис был разрешен, нам бы вообще не понадобился GetMethod (). Но на вопрос «как мне написать« GenericMethod <myType> »? Я думаю, что ответ должен включать способ избежать потери связи во время компиляции с GenericMethod. Теперь, если этот вопрос является распространенным или нет, я не знаю, но Я точно знаю, что у меня была именно эта проблема вчера, и именно поэтому я приземлился в этом вопросе
Адриан Галлеро
20
Вы могли бы сделать GenMethod.Method.GetGenericMethodDefinition()вместо this.GetType().GetMethod(GenMethod.Method.Name). Это немного чище и, вероятно, безопаснее.
Даниэль Кэссиди
Что означает «myType» в вашем образце?
Разработчик
37
Теперь вы можете использоватьnameof(GenericMethod)
dmigo
140

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

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

Если вы хотите вызвать универсальный метод, для которого при «нормальном» использовании был бы сделан вывод о его типе, то это просто сводится к приведению объекта неизвестного типа 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оператора).

Приведя аргумент объекта к 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класс не реализует этот интерфейс). Но в этом все дело. Компилятор не имеет информации, что этот вызов действителен. Вы, как программист, знаете это, и вы должны убедиться, что этот код работает без ошибок.

Возвращаемый тип

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

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

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

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

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

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

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

В C # 4.0 отражение не требуется, поскольку DLR может вызывать его, используя типы времени выполнения. Поскольку использование библиотеки DLR является своего рода динамической болью (вместо того, чтобы компилятор генерировал для вас код C #), среда с открытым исходным кодом Dynamitey (.net стандарт 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}));
jbtule
источник
13

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

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

TLDR: вызов известного универсального метода с объектом типа может быть выполнен с помощью:

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

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

(Действие) соответствует сигнатуре вызываемого метода т.е. ( 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();

В C # имя метода, то есть «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);
Grax32
источник
8

Никто не предоставил « классическое отражение », так что вот полный пример кода:

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
Димитр Новатчев
источник
2

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

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

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

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

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

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

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

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

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

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

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

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

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

И вы сделали. Работает шарм!

ОБНОВИТЬ:

Как подчеркнул @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.

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

Вдохновленный ответом Enigmativity - давайте предположим, что у вас есть два (или более) класса, например

public class Bar { }
public class Square { }

и вы хотите вызвать метод Foo<T>с Barи Square, который объявлен как

public class myClass
{
    public void Foo<T>(T item)
    {
        Console.WriteLine(typeof(T).Name);
    }
}

Затем вы можете реализовать метод Extension, например:

public static class Extension
{
    public static void InvokeFoo<T>(this T t)
    {
        var fooMethod = typeof(myClass).GetMethod("Foo");
        var tType = typeof(T);
        var fooTMethod = fooMethod.MakeGenericMethod(new[] { tType });
        fooTMethod.Invoke(new myClass(), new object[] { t });
    }
}

При этом вы можете просто вызвать Fooкак:

var objSquare = new Square();
objSquare.InvokeFoo();

var objBar = new Bar();
objBar.InvokeFoo();

который работает для каждого класса. В этом случае он выведет:

Квадратный
бар

Matt
источник