it-roy-ru.com

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

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

Рассмотрим следующий пример кода - внутри метода 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>()
    {
        //...
    }
}
961
Bevan

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

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

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

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

1042
Jon Skeet

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

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

153
Adrian Gallero

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

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

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

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

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

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

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

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

125
Mariusz Pawelski

В 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}));
16
jbtule

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

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

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

Никто не предоставил решение " 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
6
Dimitre Novatchev

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

Предположим, ваш метод определен следующим образом в классе 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.

2
Thierry