Дървета от изрази (expression trees)
Както видяхме в предишната точка, ламбда
изразите са разширение на анонимните методи. А анонимните методи, като всеки
метод, могат да се сведат до променлива чрез използване на делегати. В този ред
на мисли, ако имаме променлива, която представлява метод, то съответно можем да
имаме и масиви от методи. А защо не и колекции от методи (делегати)!?
По този начин може да се зададе път на
обработка на определени данни по време на изпълнение на програмата. А след като
имаме масиви и колекции от методи, защо да не може да се построи дърво от
методи!? Всъщност точно това предствавляват дърветата от изрази: познатото
дърво, като структура от данни, но вместо данни разполагаме с методи.
Както повечето от вас вече сигурно се досещат
след като стигнахме до дърветата, върху тях може да приложим всички алгоритми,
оптимизации, обхождания и др., които съществуват в теорията и практиката. (
Това е добра причина да изтърскаме праха от основните алгоритми за работа в дървета,
с които ни занимаваха в университета J )
LINQ предлага клас, с който методите вече се
третират като данни от компилатора. Например ако напишем следният код:
|
public delegate long Operation(long first, int second);
private void Test ()
{
Operation op = (x,y)
=> x+y;
long res =
op.Invoke(2,5);
}
|
То компилаторът ще
генерира:
|
public delegate long Operation(long first, int second);
private void Test()
{
Operation op = delegate (long x, int y) {
return x + y;
};
long res = op((long) 2, 5);
}
|
Това изглежда само
като синтактично подобрение. Ако обаче използваме класът Expression<T> (System.Expressions.Expression<T>
от асемблито System.Query.dll), за да обвием метода и напишем:
|
static Expression<Func<int, int, long>>
sumExpr = ( x, y) => x + y;
|
То след компилация
ще имаме:
|
private static Expression<Func<int, int, long>>
sumExpr;
static Form1()
{
ParameterExpression x;
ParameterExpression y;
sumExpr
= Expression.Lambda<Func<int, int, long>>(
Expression.Convert(
Expression.Add(
x = Expression.Parameter(typeof(int), "x"),
y = Expression.Parameter(typeof(int), "y")),
typeof(long)),
new ParameterExpression[] { x, y
});
}
|
Малко по-различно от горният пример, нали!?
Нека да обясним какво написахме, та компилатора го разбра по такъв начин.
Нека да започнем с типа, който подахме на Expression<T>: Func<int, int, long>. След като имаме структури от данни, които всъщност съдържат методи,
то трябва по някакъв начин да разбираме методите, които имаме – трябва да знаем
броя на параметрите, които се приемат, техният тип, и типа на връщаният
резултат.
За целите на LINQ
в асемблито System.Query.dll ( пространството от имена System.Query) има дефинирани следните делегати:
|
public delegate T Func<T>()
public delegate T Func<A0,
T>(A0 arg0)
public delegate T Func<A0, A1,
T>(A0 arg0, A1 arg1)
public delegate T Func<A0, A1,
A2, T>(A0 arg0, A1 arg1, A2 arg2)
public delegate T Func<A0, A1,
A2, A3, T>(A0 arg0, A1 arg1, A2 arg2, A3 arg3)
|
Както забелязвате във триъгълните скоби са
типовете на параметрите (A0, A1,…), последният е типа на връщаният резултат. Изглежда това са дефинициите
на най-използваните методи, но това не означава, че не може да подадете на Expression<T> делегат от ваш тип.
Нека да разгледаме какво се случва в
статичният конструктор ( това е генериран от компилатора код ):
·
На първите два реда се декларират параметрите (x и y) от тип ParameterExpression.
·
На следващият ред се задава израз от тип Expression.Lambda<Func<int, int, long>>. За да разберем същността на това действие, ще го разгледаме от вътре
навън:
o
Добавя се нов израз от тип BinaryExpression чрез Expression.Add, като се задават параметрите
и техният тип.
o
След това с помощта на метода Expression.Convert expr1 се конвертира до обект от тип Expression като се задава и
тип на резултата.
o
И накрая чрез Expression.Lambda<Func<int, int, long>> се създава обект от тип LambdaExpression, като се подават
инстанциите на създадените параметри.
За да е по-лесно за
четене същият код може да се престави и така:
|
ParameterExpression x;
ParameterExpression y;
BinaryExpression expr1 = Expression.Add(
x = Expression.Parameter(typeof(int), "x"),
y = Expression.Parameter(typeof(int), "y"));
Expression expr2 = Expression.Convert(expr1,typeof(long));
sumExpr1 = Expression.Lambda<Func<int, int, long>>(
expr2, new ParameterExpression[] { x, y
});
|
И накрая изразите
могат да се използват по следният начин:
|
Func<int, int, long> func =
sumExpr.Compile();
long res =
func(3,5);
|
Както, може би, се досещате дърветата от
изрази могат да се използват за динамично генериране на изпълним код.
В тази точка не разгледахме конкретни
практически примери, но дърветата от изрази са важна част от LINQ и C# 3.0 и без тях голяма част от нововъведенията
не биха били възможни. При работа с голяма част от разширяващите методи и LINQ
to SQL, LINQ to Object и др. може да забележим, че заявките се
преубразуват от дървета от изрази, които се изпълняват в момента на извикване.
Следваща част: LINQ в детайли