понедельник, 21 июня 2010 г.

Немного о динамических вызовах

Так сложилось, что иногда приходится писать код вида подобного вида:
if (action is SetPropertyAction)
{
   cond.AddRange(GenerateAction((SetPropertyAction)action));
}
else if (action is CommandAction)
{
   cond.AddRange(GenerateAction((CommandAction)action));
}
else if (action is FocusAction)
{
   cond.AddRange(GenerateAction((FocusAction)action));
}
else if (action is TransitionEffectAction)
{
   cond.AddRange(GenerateAction((TransitionEffectAction)action));
}
else if (action is NavigationAction)
{
   blockBody.AddRange(GenerateAction((NavigationAction)action));
}
В тех случаях, когда внести код GenerateAction в интерфейс было бы неверно или невозможно (например в качестве action может придти int), а разделение по типу необходимо возникает вопрос, каким же именно образом можно избавится от портянки?
Для начала создадим сферического коня в вакууме для последующих пыток:
internal abstract class A{}
internal class B : A{}
internal class C : A{}
internal class D : A{}

internal class Worker
{
  public int Foo(B c)
  {
    return 1;
  }
  public int Foo(C c)
  {
    return 2;
  }
  public int Foo(D c)
  {
    return 3;
  }
}
Задача проста: сделать функцию, принимающую экземпляр класса A в качестве параметра и вызывающая «правильный» метод класса Worker.
Вот она классическая портянка
public static int FooClassic(A a)
{
  if (a is B)
  {
    return worker.Foo((B)a);
  }
  else if (a is C)
  {
    return worker.Foo((C)a);
  }
  else if (a is D)
  {
    return worker.Foo((D)a);
  }
}
Замечательная вещь. Работает, но в случае если количество вариантов будет плодиться ужас лютый. А если имеется у данных классов появятся наследники для части из которых нужно создавать метод отличный от метода папочки…
Cтрашные вещи в Датском королевстве могут творится. Может быть можно сделать как-то по другому?

Что сразу приходит на ум: рефлекшен. Можно же просто получить нужный метод после чего его вызвать. Всё просто и логично. С небольшой натяжкой можно сказать что красиво.
public static int FooReflection(A a)
{
  return (int) worker.GetType().GetMethod("Foo", new[] { a.GetType() }).Invoke(worker, new[] { a });
}
Но есть одна небольшая проблема. Это долго. Заставим сферического коня немного поскакать.
const int repeatCount = 1000000;
var listObjects = new List<A> { new B(), new C(), new D() };
И дальше замер скорости кода:
for (var i = 0; i < repeatCount; i++)
{
  foreach (var obj in listObjects)
  {
    FooClassic(obj);
  }
}
Если FooClassic у меня выдаёт порядка 117 миллисекунд, то FooReflection уже тратит порядка 7 секунд. В некоторых местах ухудшение времени в 65 раз не является критичным (например, если метод за всё время вызывается пару десятков раз), но может быть можно побыстрее?
Что будет, если мы сразу станем запоминать, для какого типа был вызыван метод?
public static TResult InvokeMethod<T, TArg, TResult>(this T obj, string name, TArg arg)
{
  var key = new ComposedKey(obj, name, arg.GetType());
  Delegate value;

  if (!_methodsCache.TryGetValue(key,out value))
  {
    var parametr = Expression.Parameter(typeof (TArg), "x");
    var body = Expression.Call(Expression.Constant(obj), name, new Type[] {}, Expression.TypeAs(parametr, arg.GetType()));
    var expression = Expression.Lambda<Func<TArg, TResult>>(body, parametr);
    value = expression.Compile();
    _methodsCache[key] = value;
  }
  return ((Func<TArg, TResult>) value)(arg);
}
ComposedKey в данном случае ключ, который позвляет запоминать последовательность объектов. По имени свойства и типу апгумента можно создать функцию, вызывающую необходимый метод. В данном случае expression.Compile как раз создаёт такой метод. Теперь можно написать функцию:
public static int FooInvoker(A a)
{
  return worker.InvokeMethod<Worker, A, int>("Foo", a);
}
Такой подход позволяет ещё немного сократить время. И теперь выполнение занимает порядка 1 секунды. Небольшое, но улучшение.
В четвёртом фреймворке появился новый объект - dynamic, вызовы которого всегда происходят динамически и выбирается наиболее подходящий метод. В данном случае можно попробовать воспользоваться именно этим классом.
public static int FooDynamic(A a)
{
  return worker.Foo((dynamic) a);
}
Это самый короткий и элегантный вариант, но по времени он всё равно отстаёт от классического решения.
Результаты:
Имя методаСкорость в миллисекундах
Classic117
Reflection6994
Invoker1112
Dynamic907

Таким образом, если скорость исполнения какого-то кода не сильно важна, то можно воспользоваться каким-либо из методов, по облегчению себе жизни. Особенно это будет полезно, если планируется серьёзное увеличение числа классов, для которых существуют перегруженные методы. В остальных же случаях лучше не пытаться взрывать себе мозг и поступать наиболее просто.

Исходные коды можно скачать по ссылке

Комментариев нет:

Отправить комментарий