Виклик загального методу з параметром типу, відомим лише під час виконання, можна значно спростити, використовуючи 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.