Как использовать рефлексию для вызова универсального метода? - PullRequest
958 голосов
/ 24 октября 2008

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

Рассмотрим следующий пример кода - внутри метода 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 ]

1040 голосов
/ 24 октября 2008

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

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

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

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

152 голосов
/ 27 февраля 2011

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

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. Я не знаю, есть ли лучший способ.

125 голосов
/ 16 марта 2014

Вызов универсального метода с параметром типа, известным только во время выполнения, может быть значительно упрощен при использовании типа 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 });
}

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

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

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

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

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

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

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

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

16 голосов
/ 05 июля 2011

В 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}));
10 голосов
/ 10 января 2015

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

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

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);
5 голосов
/ 24 августа 2016

Никто не предоставил решение " classic Reflection ", поэтому вот полный пример кода:

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 голосов
/ 23 октября 2015

Это мои 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 });

И все готово. Работает шарм!

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.

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