Дървета от изрази (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 в детайли