четверг, августа 28, 2008

работа с C# Expression trees

Иногда стопроцентно известна сигнатура конструктора класса (соглашение такое, скажем), но сам класс неизвестен.
Например, потому что этот класс - параметр дженерик-метода. А у дженериков в качестве ограничения типа можно задать только конструктор без параметров, что не гуд.
Тогда приходится использовать reflection.
Например, через класс Activator.
Но использование reflection'а имеет некоторый (хотя и обычно терпимый) оверхед, плюс, оно просто не интересно =), поэтому можно попробовать иначе.
Воспользоваться так называемыми деревьями выражений, появившимися в .NET BCL 3.5. Они позволяют динамически строить и анализировать (как это делают провайдеры LINQ) AST выражений (в том числе, соответствующих делегатам). А потом выражения для делегатов можно откомпилировать в CIL (который затем JIT откомпилирует в native-код). 
Самое лучшее - когда типы, используемые в выражении известны. 
Тогда построения дерева выражения можно возложить на компилятор, написав что-то вроде:

Expression<Func<char[],string>> e = chars => new string(chars);

Но если бы типы были известны, вообще делать было бы нечего. Поэтому нам придется строить дерево вручную. Что, в общем-то, тоже несложно.
Легко увидеть, что дерево выражения для данного конструктора имеет вид (в псевдокоде)

(lambda (chars)(new (get-constructor (typeof string) (typeof chars[])) chars)

В реальном коде на C# это будет выглядеть как:

var p = Expression.Parameter(typeof(char[]), "chars");
var lambda = Expression.Lambda<Func<string>>(
    Expression.New(typeof(string).GetConstructor(typeof(char[]), p);

аналогично и для других конструкторов: 

new T(params) ->
(lambda (params) (new
  (get-constructor (typeof T) (map (lambda (p) (typeof p)) params)
  params)


и дерево будет выглядеть как
var parameters = (from t in argTypes
                  select Expression.Parameter(t, "@"+t))
                  .ToArray();
Expression.Lambda(
  Expression.New(targetType.GetConstructor(argTypes), parameters),
  parameters);


N.B.: ВАЖНО, чтобы параметры в теле лямбда-функции совпадали с ее параметрами, т.к. компилятор ищет их именно по совпадению, а не по имени.
Как видно, реальный код хотя и отличается, но не очень значительно.
Я наваял небольшой класс, кэширующий скомпилированные конструкторы. Замеры показали, что однократная компиляция+извлечение из словаря обгоняют чистый рефлекшен где-то с 16 тыс. созданий объектов, скэшированный же явным образом результат вообще вне конкуренции, т.к. это просто вызов конструктора.
В общем, expression trees могут оказаться хорошей заменой System.Reflection.Emit или System.CodeDom.Compiler там, где надо сгенерировать код на лету, но до порождения CIL или генерации кода в виде текста напрямую спускаться не хочется.
При этом, возможности expression trees ограничены - в них сложно обеспечить последовательное выполнение команд (разве что используя для этого AndAlso) и обработку исключений, нельзя породить класс (не для этого они созданы) и пр.
Опять же - не для того они были сделаны =)
blog comments powered by Disqus