Как стать автором
Обновить

Быстрый Reflection через DynamicMethod

Время на прочтение5 мин
Количество просмотров2.2K
Одной из «вкусных» возможностей .NET, а в частности C#, является Reflection(рефлекшн) — богатый набор классов для работы с типами данных, их свойствами, методами, полями во время выполнения.
Но за такой широкий функционал приходится платит производительностью.
Вызов метода из кода выполняется практически в сто раз быстрее чем вызов через рефлекшн.
А если мы заранее не знаем какой конкретный класс будет использоваться и какой метод будет вызван или не хотим привязываться к конкретной библиотеке, то работа через рефлекшн может сильно снизить производительность приложения.

В данной статье я хотел бы рассказать об использовании динамических методов для таких целей.


Допустим, что мы заранее знаем лишь количество аргументов метода который хотим вызвать. Этого нам будет достаточно…
Напишем тестовый класс, метод которого мы будем вызывать.

using System;

namespace AppForBlog1 {
  class TestClass {
    int index;
    public int Index { get { return index; } set{ index = value; } }
    public int UpdateIndex(int index) {
      this.index = index;
      return this.index * 2;
    }
  }
}


* This source code was highlighted with Source Code Highlighter.

Что мы хотим? Мы хотим получить делегат нового метода, который на вход должен получать экземпляр нужного класса и параметры вызываемого метода и возвращать результат вызываемого метода.

Сначала необходимо определить делегат.

public delegate object MethodDelegate(object inst, object param);

Теперь определимся какая информация необходима нам для вызова метода: имя класса, имя метода, сигнатура метода, и тип класса откуда этот метод будет вызываться(объясню ниже).

Переходим к самому интересному.
Для работы с рефлекшн и создания динамического метода нам необходимо использовать следующие пространства имен соответственно System.Reflection и System.Reflection.Emit.

Во-первых, нам необходимо получит информацию о методе, который мы будем вызывать. Сделаем мы это средствами обычного рефлекшна.

MethodInfo mi = тип_класса.GetMethod( имя_метода, BindingFlags.Public | BindingFlags.Instance, null, new Type[] { тип_параметра }, null);

Во-вторых, нужно создать экземпляр класса DynamicMethod.

DynamicMethod dm = new DynamicMethod( имя_нового_метода, атрибуты_метода, способ_вызова, тип_результата_нового_метода , new Type[] { тип_параметра_1_нового_метода, тип_параметра_2_нового_метода }, тип_класса_владельца);

Здесь следует уточнить, что метод не может существовать без какого-либо класса, поэтому мы указываем тип_класса_владельца, к которому метод будет прикреплен.
Мы будем создавать статический метод, поэтому в атрибутах_метода следует указать MethodAttributes.Public и MethodAttributes.Static, а в способе_вызова — CallingConventions.Standard.

Далее наш метод должен что-то делать. Поэтому создаем IL-генератор и заносим в него инструкции.

ILGenerator gen = dm.GetILGenerator();

Вызываем мы метод не статический, поэтому первым аргументом должен быть экземпляр класса, после него идут параметры.
Т.к. экземпляр и параметры приходят в метод упакованными в object, то их нужно распаковать (выполнить unboxing).
После вызова метода нам нужно уже запаковать результат в object и вернуть вызывающему коду.

gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Unbox_Any, тип_класс);
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Unbox_Any, тип_параметра);
gen.Emit(OpCodes.Callvirt, mi);
gen.Emit(OpCodes.Box, тип_результата);
gen.Emit(OpCodes.Ret);


Метод готов. Осталось только получить делегат.

MethodDelegate method = (MethodDelegate)dm.CreateDelegate(typeof(MethodDelegate));

Теперь этот делегат мы можем использовать сколько угодно раз для вызова нужного нам метода.

Соберем вышеописанное в класс.

using System;
using System.Reflection;
using System.Reflection.Emit;

namespace AppForBlog1 {
  public class ReflectClass {
    public static MethodDelegate GetMethodDelegate(Type owner, Type instance, string methodName, Type returnType, Type parameterType) {
      //Находим информацию о методе
      MethodInfo mi = instance.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance, null, new Type[] { parameterType }, null);
      if(mi == null) return null;
      //Создаем динамический метод
      DynamicMethod dm = new DynamicMethod(methodName + "Wrapper",
            MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard,
                typeof(object), new Type[] { typeof(object), typeof(object) }, owner, false);
      //Создаем IL-генератор
      ILGenerator gen = dm.GetILGenerator();
      //Кладем на стек указатель на экземпляр класса
      gen.Emit(OpCodes.Ldarg_0);
      //Выполняем распаковку из object
      gen.Emit(OpCodes.Unbox_Any, instance);
      //Кладем на стек параметр
      gen.Emit(OpCodes.Ldarg_1);
      //Выполняем распаковку из object
      gen.Emit(OpCodes.Unbox_Any, parameterType);
      //Вызываем метод
      gen.Emit(OpCodes.Callvirt, mi);
      //Запаковываем результат в object
      gen.Emit(OpCodes.Box, returnType);
      //Вызвращаем результат
      gen.Emit(OpCodes.Ret);
      //Создаем делегат
      return (MethodDelegate)dm.CreateDelegate(typeof(MethodDelegate));
    }
  }

  public delegate object MethodDelegate(object inst, object param);
}

* This source code was highlighted with Source Code Highlighter.

Ну и пора проверить работоспособность данного класса.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
namespace AppForBlog1 {
  [TestFixture]
  public class TestFixtureClass {
    [Test]
    public void FastReflectionTest() {
      //Создаем экземпляр "неизвестного класса"
      TestClass ts = new TestClass();

      //Вызываем напрямую
      Assert.AreEqual(4, ts.UpdateIndex(2));
      Assert.AreEqual(2, ts.Index);

      //Получаем делегат
      MethodDelegate updateIndex = ReflectClass.GetMethodDelegate(typeof(TestFixtureClass), typeof(TestClass), "UpdateIndex", typeof(int), typeof(int));
      
      ts.Index = 1;
      Assert.AreEqual(1, ts.Index);

      //Вызываем делегат
      Assert.AreEqual(16, updateIndex(ts, 8));   
      Assert.AreEqual(8, ts.Index);
    }
  }
}

* This source code was highlighted with Source Code Highlighter.

Все работает!..

При использовании в реальных приложениях типы придется получать не через typeof, а соответственно опять через рефлекшн. Но это нужно будет сделать один раз и потом пользоваться только делегатом, скорость вызова которого всего в 2 раза медленнее чем прямой вызов.

Удачи!

P.S. Данный способ также работает и для свойств классов, т.к. любое свойство — это два метода: set и get. А их можно получить из информации о свойстве (PropertyInfo).
Теги:
Хабы:
Всего голосов 19: ↑16 и ↓3+13
Комментарии7

Публикации