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


1069

Який найкращий спосіб викликати загальний метод, коли параметр типу не відомий під час компіляції, а натомість отримується динамічно під час виконання?

Розглянемо наступний зразок коду - всередині 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
Я спробував рішення Джона і не зміг змусити його працювати, поки я не зробив загальний метод оприлюдненим у своєму класі. Я знаю, що інший Джон відповів, сказавши, що вам потрібно вказати обов'язкові позначки, але це не допомогло.
nakew

12
Вам також потрібно BindingFlags.Instanceне тільки BindingFlags.NonPublicотримати приватний / внутрішній метод.
Ларс Кемман

2
Сучасна версія цього питання: stackoverflow.com/q/2433436/103167
Бен Войгт

@Peter Mortensen - fyi Я використав пробіли перед "?" відокремити англійські частини від неанглійських (C #) частин; IMHO видалення місця робить це схожим на? є частиною коду. Якби не було коду, я, безумовно, погодився б видалити пробіли, але в цьому випадку ...
Беван

Відповіді:


1137

Вам потрібно використовувати роздуми, щоб почати метод, а потім "сконструювати" його, надавши аргументи типу за допомогою 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клас не реалізує цей інтерфейс). Але в цьому вся суть. У компілятора немає інформації про те, що цей виклик дійсний. Ви, як програміст, це знаєте, і ви повинні переконатися, що цей код працює без помилок.

Тип повернення gotcha

Коли ви викликаєте недійсний метод з параметром динамічного типу, його тип повернення, ймовірно, буде 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вам відкладіть цю перевірку на час виконання.
Mariusz Pawelski

Чудова відповідь та пояснення, прекрасно працює для мене. Набагато краще, ніж прийнята відповідь, коротше писати, більш виконавська та безпечніша.
ygoe

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

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

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.


Аргументи, щоб 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);
    }
}

Тоді ви можете реалізувати такий метод розширення, як:

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

який працює для кожного класу. У цьому випадку він виведе:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.