Expression Trees - 1. daļa
29. septembra Microsoft Partneru Konferences laikā Klausoties B. Zaas lekciju par C# 3.0 jaunākajām un jocīgākajām iespējām, atcerējos par izteiksmju kokiem jeb veidu, kā no lambda izteiksmes iegūt datu struktūru, kas apraksta šo matemātisko izteiksmi. Bet nu par visu pēc kārtas.
Expression Tree ir veids ar kura palīdzību ir iespējams no izpildāma koda (piemēram, LINQ izteiksmes) iegūt datus. Šis paņēmiens ir ērts, lai no LINQ izteiksmes ģenerētu SQL izteiksmes, kas tiek izpildītas uz servera. Nedaudz aizskrēju pa priekšu. Tātad, sāksim ar pavisam vienkāršu izteiksmes koda izveidošanu:
Func<int, int, int> function = (a, b) => (a + b);
Augstāk minētajā piemērā ir 3 daļas:
- Func<T1, T2, TResult> deklarēšana;
- Vienādības operātors: =;
- Lambda izteiksme: (a, b) => (a +b);
Mainīgais function tagad norāda uz izpildāmu kodu, kas prot vienu ciparu pieskaitīt pie otra. Tas ir apmēram tas pats, kas uzrakstīt funkciju:
public int function(int a, int b)
{
return a + b;
}
Funkciju vai lambda izteikmsi var izsaukt šādi:
int c = function(4, 4);
Augstāk mēs redzējām, kā mēs varam izveidot mainīgo, kas norāda uz izpildāmu kodu. No šī izpildāmā koda var iegūt datu struktūru, kas apraksta izteiksmi - izteiksmes koku. Jāpievieno ir System.Linq.Expressions vārdu telpa, lai piekļūtu nepieciešamajām bibliotekām.
Expression<Func<int, int, int>> expression = (a, b) => a + b;
Visual Studio 2008 piemēros var atrast Expression Tree Visualizer rīku, kas palīdz attēlot grafiski, kāda ir izteiksmu koka struktūra.
Lambda izteiksmēm ir speciāla Compile() metode, kas no datu struktūras pārvērst atpakaļ to izpildāmā kodā un to izpilda. Piemēram, koda rindiņa atgriezīs 8:
expression.Compile()(4, 4);
Kāpēc tad īsti ir nepieciešami šie izteiksmju koki? Viens no plašāk izmantojamajiem mērķiem ir LINQ izteiksmes translēšana uz SQL izteiksmi. Piemēram,
var query = from c in db.Customers
where c.City == "London"
select new { c.City, c.CompanyName };
uz
SELECT [t0].[City], [t0].[CompanyName]
FROM [dbo].[Customers] AS [t0]
WHERE [t0].[City] = @p0
Evaluējot tālāk domu par izteiksmju kokiem ienāca prātā, ka arī man būtu jāievēro Greenspun 10-tais programmēšanas likums: "Any sufficiently complicated C or Fortran program contains an ad-hoc, informally-specified bug-ridden slow implementation of half of Common Lisp."
Radās ideja implementēt pavisam vienkāršas Lisp izteiksmes interpretatoru un izpildītāju. Tātad Lisp matemātiskās operācijas sākas ar operatoru pēc kura seko visi pārējie operandi. Apmēram šādi: (+ 4 4).
Ieejas parametru nolasīšanai izveidojam ciklu:
Interpreter interpreter = new Interpreter();
string input = null;
while (true)
{
Console.Write("> ");
input = Console.ReadLine();
if (string.Compare(input, "(quit)", true) == 0)
{
break;
}
interpreter.Input = input;
Console.WriteLine(interpreter.Compile());
}
Tālāk ir jāķeras klāt pie interpretatora izveides. Par cik pirmajā raksta daļā uzdevums ir pavisam vienkāršs un lai nebūtu jānodarbojas ar ieejas parametru pamatīgu ķidāšanu, tad neaizrausimies ar iekļauto operāciju izpildi, kādā no operandiem, piemēram, (+ 4 (* 2 2)).
Izveidojam kodu, kas mums sadala operāciju un operandus (pievēršam uzmanību, ka šajā alfa versijā ir pieejams tikai operācijas ar skaitļiem līdz desmit (10) to neieskaitot :). Lielāki skaitļi tiks atbalstīti nākamajā CTP versijā).
string expressionType = this.Input.Substring(1, 1);
string leftSideValue = this.Input.Substring(3, 1);
string rightSideValue = this.Input.Substring(5, 1);
Pēc tam ir nepieciešams izveidot mērķa izteiksmju kokam labo un kreiso pusi, t.i., parametrus, kuriem tiks pielietotas vērtības lambda izteiksmes izpildes laikā.
var left = Expression.Parameter(typeof(int), "l");
var right = Expression.Parameter(typeof(int), "r");
Atkarībā no operācijas ir nepieciešams izveidot attiecīgo izteiksmes ķermeni:
switch (expressionType)
{
case "+":
body = Expression.Add(left, right);
break;
case "-":
body = Expression.Subtract(left, right);
break;
case "/":
body = Expression.Divide(left, right);
break;
case "*":
body = Expression.Multiply(left, right);
break;
case ">":
body = Expression.GreaterThan(left, right);
break;
case "<":
body = Expression.LessThan(left, right);
break;
default:
throw new InvalidOperationException("Operator is not supported.");
}
Šeit gan ir jāatzīmē, ka attiecīgās operācijas izvēli nevar veikt kaut kā dinamiskāk - t.i., nepieciešamā operācija ir jāveido izsaucot attiecīgo statisko metodi.
Pēc sekmīgas lambda izteiksmes izveides, varam to kompilēt uz iegūt Delegate tipa objektu, kuru vēlāk varam arī dinamiski izsaukt ar nepieciešamajiem parametriem:
exp = Expression.Lambda(body, new[] { left, right });
var i = ((LambdaExpression)exp).Compile();
return i.DynamicInvoke(
Convert.ToInt32(leftSideValue),
Convert.ToInt32(rightSideValue)).ToString();
"Sistēmas" darbība nedaudz atgādina REPL :) Ir ļoti ierobežota savā darbībā un piedāvātajos servisos, bet tāds arī bija šīs pirmās daļas mērķis par izteiksmju kokiem.
Ja kādam interesē pilns izejas kods, tas atrodams šeit.
Otrajā daļā mēģināsim šai pieejai sameklēt arī kādu reālāku pielietojumu ikdienas nopietnā biznesa aplikācijā.
Cerams, ka kaut kur noderēs!