Kiedyś szczytem wszystkich wynalazków stworzonych przez programistyczne języki funkcjonalne była możliwość wykonania funkcji, gdy ona jest w formie napisu string.
Ten napis można było zmieniać i później go wywołać jak zwyczajny kod.
Inaczej mówiąc "eval" .
Wiele języków programowania wspiera operację „eval”, czyli możliwość wywołania funkcji w danym języku, gdy dynamicznie tworzymy wyrażenie tej własnej funkcji.
Najprostszy eval jak powiedzieliśmy wcześniej wymaga do operacji zwykłego napisu string. Języki, które to wspierają pozwalają na wywołanie takiego kodu. Oto przykładowy kod JavaScript.
var x = 10;
var y = 20;
var a = eval("x * y") + "<br>";
var b = eval("2 + 2") + "<br>";
var c = eval("x + 17") + "<br>";
Eval pozwala na użycie samej składni języka. Taka wywołana funkcja może uzyskać dostęp do zmiennych, funkcji, klas itp., jeśli istnieją one w obecnym kontekście.
Poniżej znajduje się przykład z Pythona i EVAL.
def square(x):
return x*x
>>> square(10) + 50
150
>>> eval("square(10) + 50")
150
Mechanizm EVAL jest prosty. Umożliwia programowi wywołanie dynamicznie utworzonych wyrażeń. W wielu językach programowanie jest to główny fundament wykonawczy lub operacyjny systemu.
Mechanizmy te od środka są łatwiejsze do obsługi w językach dynamicznych takich jak Python. Nie są to języki kompilowane jak sama nazwa wskazuje “dynamiczne”.
W kompilowanych językach programowania jak C# sprawy trochę się komplikują.
Dotychczasowy kompilator C# został napisany w języku c/c++ a on nie dawał dużych opcji w wywołaniu kodu w trakcie jego wywołania.
Obecnie, jeśli jesteś na czasie i masz Visual Studio 2015 jest możliwe wywołanie kodu ze zwykłego napisu. Wszystko dzięki kompilatorowi Roslyn i projekcie ScriptCS. W pewnym sensie wyrażenie „eval” istnieje w C#.
Wszystko dzięki kompilatorowi Roslyn, który już został napisany w C#. Sam kompilator Roslyn i jego biblioteki są dostępne więc można tworzyć kod, który analizuje kod.
Na ten temat napisałem wcześniej wpis jak nie cały cykl.
Dzisiaj skoncentrujmy się na wynalazku, który jest od .NET 3.5, czyli na wyrażeniach drzewiastych.
Jeśli nie chcesz czytać artykuł zawsze możesz obejrzeć mój filmik na YouTube
Wyrażenie drzewiaste
Spójrz na proste wyrażenie lambda.
Func<int, int, int> sub = (x, y) => x - y;
Jak już wiesz w ten sposób utworzyliśmy lokalną funkcję, która przyjmuje dwa parametry.
int result = sub(30, 20);
// result is now 10
Wyrażenia lambda, są skompilowanym kodem.
Aby stworzyć wyrażenie drzewiaste tej funkcji, musimy zmienić składnię tylko trochę.
Expression<Func<int, int, int >> subExpr =
(x, y) => x - y;
W parametrze generycznym Expression umieszczamy wyrażenie delegatę.
Teraz w kompilator stworzy zupełnie inny kod IL. Ten kod będzie zawierał informację o obiektach i ich typach użytych wewnątrz tego wyrażenia.
Zmienna przetrzymująca wyrażenie jest teraz abstrakcyjnym reprezentantem wyrażenia lambda i nie może zostać wywołana bezpośrednio.
Wyrażenie musi zostać skompilowane przed użyciem.
Func <int, int, int> subCompiled = subExpr.Compile();
Jak widzisz bez żadnych bibliotek jak Roslyn czy ScriptCS masz w swoim kodzie możliwość obsługi swojego własnego kodu.
Ten przykład oczywiście jest bezużyteczny.
Co jednak powiesz na dynamiczne utworzenie wyrażenia i jego analizowanie w trakcie działania programu.
Wyrażenia drzewiaste są więc skrótem do zabawy z kompilatorem i w .NET 3.5, 4.0 była to jedyna przyjazna opcja do zabawy ze swoim własnym kodem.
Analizowanie wyrażeń
Główną ideą wyrażeń drzewiastych jest traktowanie samego kodu jako danych, które mogą być analizowane w trakcie działania programu.
Jest to często przydatne w celach logowaniu i debugowania.
public static void Main(string[] args)
{
Person p = new Person() { Name = "Zbigniew" };
Expression<Func<int, int, int, Person, string>> methodExpr =
(x, y, z, b) => x.ToString() + y.ToString() + z.ToString() + b.Name;
var parameters = methodExpr.Parameters;
Console.WriteLine("Parameters : ");
foreach (var item in parameters)
{
Console.Write("\t There is a parameter : (" +item);
Console.Write(") is ByRef " + item.IsByRef);
Console.WriteLine();
}
var body = methodExpr.Body;
Console.WriteLine("Body : ");
Console.WriteLine("\t" + body);
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Aby jednak pokazać bardziej dokładnie wyrażenie, trzeba napisać bardziej skomplikowany kod, ale na tym prostym przykładzie w łatwy sposób możemy uzyskać dostęp do parametrów występujących w wyrażeniu, jak i jego ciała.
Można oczywiście przeanalizować wyrażenie lepiej, ale wymaga to ciągłego zbadania czym jest obecne wyrażenie. A po sprawdzeniu tego trzeba było sprawdzić, czy wewnątrz tego wyrażenia są kolejne wyrażenia.
Sprawa jest bardzo łatwa, jeśli spodziewamy się wyrażenia o określonym już wzorze.
Expression<Func<int, int, int>> expression =
(x, y) => x + y;
BinaryExpression body = (BinaryExpression)expression.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ParameterExpression right = (ParameterExpression)body.Right;
Console.WriteLine(expression.Body);
Console.WriteLine(" The left part of the expression: " +
"{0}{4} The NodeType: {1}{4} The right part: {2}{4} The Type: {3}{4}",
left.Name, body.NodeType, right.Name, body.Type, Environment.NewLine);
W tym wypadku wiem, że wyrażenie może zostać rzutowane na BinaryExpression ze względu na to, że w wyrażenie reprezentuje proste dodawanie dwuargumentowe.
Jeśli wyrażenie nie reprezentuje wyrażenia dwuargumentowego, to zostanie wyrzucony wyjątek.
Oczywiście, jeśli już tylko dodamy wewnątrz wyrażenia wywołanie metody jak np. ToString() to już nie wszędzie rzutowanie zadziała. Chyba że zmienię kod rzutowania wyrażeń, bo wiem, że i po prawej, i po lewej stronie będzie wykonywana jakaś metoda.
Expression<Func<int, int, string>> expression =
(x, y) => x.ToString() + y.ToString();
var body = (BinaryExpression)expression.Body;
var left = (MethodCallExpression)body.Left;
var right = (MethodCallExpression)body.Right;
Console.WriteLine(expression.Body);
Console.WriteLine(" The left part of the expression: " +
"{0}{4} The NodeType: {1}{4} The right part: {2}{4} The Type: {3}{4}",
left.Method.Name, body.NodeType, right.Method.Name, body.Type, Environment.NewLine);
Oczywiście im bardziej skomplikowane wyrażenie, tym coraz bardziej sobie uświadamiasz, dlaczego nazywamy to wyrażeniami drzewiastym.
Expression<Func<int, int,string, string, string>> expression =
(x, y, z1,z2) => x.ToString() + y.ToString() + z1 + z2;
var body = (LambdaExpression)expression;
var subbody = (BinaryExpression)body.Body;
var subbodyLeft = (BinaryExpression)subbody.Left;
var subbodyLeft_Left = (BinaryExpression)subbodyLeft.Left;
var subbodyLeft_Right = (ParameterExpression)subbodyLeft.Right;
var subbodyLeft_Left_Left = (MethodCallExpression)subbodyLeft_Left.Left;
var subbodyLeft_Left_Right = (MethodCallExpression)subbodyLeft_Left.Left;
var subbodyRigth = (ParameterExpression)subbody.Right;
Oto rozbiór wyrażenia, aż do parametru albo do wywołania metody.
Wyrażenia drzewiaste są łatwe w analizie, jeśli wiemy jak dokładnie to wyrażenie będzie wyglądało, a różnica polega np. na wywołaniu innej metody wewnątrz wyrażenia bądź przesłania innego parametru wewnątrz wyrażenia.
Oto przykład użycia wyrażeń drzewiastych do przesyłania sobie informacji na temat określonej właściwości danej klasy.
public PropertyInfo GetPropertyInfo<TSource, TProperty>(
TSource source,
Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
MemberExpression member = propertyLambda.Body as MemberExpression;
if (member == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a method, not a property.",
propertyLambda.ToString()));
PropertyInfo propInfo = member.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a field, not a property.",
propertyLambda.ToString()));
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType))
throw new ArgumentException(string.Format(
"Expresion '{0}' refers to a property that is not from type {1}.",
propertyLambda.ToString(),
type));
return propInfo;
}
A to użycie tej metody.
Person per = new Person();
var propertyInfo = GetPropertyInfo(per, o => o.Age);
Mając reprezentacje w kodzie właściwości klasy mogę z niej wyciągnąć np. jej nazwę albo metodę GET czy SET.
Jest to ciekawe wykorzystanie wyrażeń drzewiastych. Zamiast np. przesyłać sobie nazwę właściwości w formacie string, to można zawsze przesłać wyrażenie odnoszące się do właściwości z tej konkretnej klasy.
Sam skorzystałem z tej techniki, gdy chciałem przesłać sobie informację, do jakiej konkretnej właściwości się odnoszę, a jedyną alternatywą było tworzenie swoich własnych atrybutów pod tymi właściwościami.
A co, jeśli chcę zdobyć informację na temat wyrażeń w sposób elastyczny? Gdy nie wiemy, czym dokładnie to wyrażenie będzie.
Można by było stworzyć metodę rekurencyjną, która by badała za każdy razem czym wyrażenie dokładnie jest i w zależności od tego wykonywałaby ona odpowiedni kod.
Ten kod jest bardzo długi.
public static void WriteExpression(Expression expression, string indent, string label)
{
switch (expression.NodeType)
{
case ExpressionType.Add:
case ExpressionType.AddChecked:
case ExpressionType.And:
case ExpressionType.AndAlso:
case ExpressionType.ArrayIndex:
case ExpressionType.Coalesce:
case ExpressionType.Divide:
case ExpressionType.Equal:
case ExpressionType.ExclusiveOr:
case ExpressionType.GreaterThan:
case ExpressionType.GreaterThanOrEqual:
case ExpressionType.LeftShift:
case ExpressionType.LessThan:
case ExpressionType.LessThanOrEqual:
case ExpressionType.Modulo:
case ExpressionType.Multiply:
case ExpressionType.MultiplyChecked:
case ExpressionType.NotEqual:
case ExpressionType.Or:
case ExpressionType.OrElse:
case ExpressionType.Power:
case ExpressionType.RightShift:
case ExpressionType.Subtract:
case ExpressionType.SubtractChecked:
BinaryExpression bi = (BinaryExpression)expression;
Console.WriteLine(indent + label + " BinaryExpression:{0}{1}{2} (",
bi.NodeType, bi.IsLifted ? ", IsLifted" : "",
bi.IsLiftedToNull ? ", IsLiftedToNull" : "");
WriteExpression(bi.Left, " " + indent, " Left: ");
WriteExpression(bi.Right, " " + indent, " Right: ");
WriteExpression(bi.Conversion, " " + indent, " Conversion: ");
break;
case ExpressionType.ArrayLength:
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
case ExpressionType.Negate:
case ExpressionType.UnaryPlus:
case ExpressionType.NegateChecked:
case ExpressionType.Not:
case ExpressionType.Quote:
case ExpressionType.TypeAs:
UnaryExpression un = (UnaryExpression)expression;
Console.WriteLine(indent + label + " UnaryExpression :{0}{1}{2} (",
un.NodeType, un.IsLifted ? ", IsLifted" : "",
un.IsLiftedToNull ? ", IsLiftedToNull" : "");
WriteExpression(un.Operand, " " + indent, " Operand: ");
break;
case ExpressionType.Call:
MethodCallExpression met = (MethodCallExpression)expression;
Console.WriteLine(indent + label + " MethodCallExpression :");
foreach (Expression item in met.Arguments)
{
WriteExpression(item, " " + indent, "-> Arguments:");
}
WriteExpression(met.Object, " " + indent, "-> Object:");
break;
case ExpressionType.Conditional:
ConditionalExpression con = (ConditionalExpression)expression;
Console.WriteLine(indent + label + " ConditionalExpression :");
WriteExpression(con.Test, indent + " ", "-> Test: ");
break;
case ExpressionType.Constant:
Console.WriteLine(indent + label + " ConstantExpression ({0})",
((ConstantExpression)expression).Value);
break;
case ExpressionType.Invoke:
InvocationExpression inv = (InvocationExpression)expression;
Console.WriteLine(indent + label + " MethodCallExpression :");
foreach (Expression item in inv.Arguments)
{
WriteExpression(item, " " + indent, "-> Argument:");
}
WriteExpression(inv.Expression, " " + indent, "-> Expression:");
break;
case ExpressionType.Lambda:
LambdaExpression lambda = (LambdaExpression)expression;
Console.WriteLine(indent + label + " LambdaExpression:");
foreach (var item in lambda.Parameters)
{
WriteExpression(item, " " + indent, "-> Parameter: ");
}
WriteExpression(lambda.Body, " " + indent, "-> Body: ");
break;
case ExpressionType.ListInit:
break;
case ExpressionType.MemberAccess:
break;
case ExpressionType.MemberInit:
break;
case ExpressionType.New:
break;
case ExpressionType.NewArrayInit:
break;
case ExpressionType.NewArrayBounds:
break;
case ExpressionType.Parameter:
Console.WriteLine(indent+label+ " ParameterExpression ({0})",
((ParameterExpression)expression).Name);
break;
case ExpressionType.TypeIs:
break;
}
}
Sprawdźmy jak on działa w praktyce
Expression<Func<int, int,string, string, string>> expression =
(x, y, z1,z2) => x.ToString() + y.ToString() + z1 + z2;
Person p = new Person() { Name = "Cezary", Age = 27 };
Expression<Func<int, int, string, string, Person, bool>> expression2 =
(x, y, z1, z2, person) => (x - y) > (z1.Length + z2.Length) || person.Age >= 26;
Console.ForegroundColor = ConsoleColor.Yellow;
WriteExpression(expression,"","");
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
WriteExpression(expression2, "", "");
Console.WriteLine();
Jak widać na upartego można napisać kod drukujący zawartość każdego wyrażenia drzewiastego.
Istnieje bardzo dużo typów wyrażeń i napisanie takiego kodu jest czasochłonne.
Co gorsza, jeśli byśmy szukali czegoś konkretnego w tym wyrażeniu, to byśmy skończyli z kolejną listą warunków „if” i „else”.
Na szczęście do badania zawartości drzewa można skorzystać z klasy abstrakcyjnej ExpressionVisitor.
Zawiera ona następujące metody.
Nie jest ich tak dużo, jak typów gałęzi w drzewach, ale użycie tej klasy w przypadkach, gdy chcemy coś znaleźć lub zamienić w wyrażeniu drzewiastym jest zalecane.
Poniżej znajduje się prosta klasa odwiedzająca wyrażenie drzewiaste. Zbierze ona informację na temat wszystkich parametrów oraz tego ile razy w wyrażeniu została wykonana operacja logiczna jak „większe równe”.
Metoda VisitBinary wykonuje się, gdy odwiedzający napotka w drzewie wyrażenie dwuargumentowe.
Za każdy razem, gdy wywołuje się metoda VisitBinary będę sprawdzał, czy dane wyrażenie dwuargumentowe nie zawiera operacji logicznej
Przy odwiedzeniu całego wyrażenia Lambda w metodzie „VisitLambda” pobieram listę wszystkich parametrów, jakie występują w tym wyrażeniu.
public class InfoExpressionVisitor : ExpressionVisitor
{
private readonly List<ParameterExpression> _parameters = new List<ParameterExpression>();
private int _number = 0;
protected override Expression VisitLambda<T>(Expression<T> node)
{
_parameters.AddRange(node.Parameters);
return base.VisitLambda(node);
}
public List<ParameterExpression> GetParameters()
{
return _parameters;
}
public int GetNumberOfLogicExpressions()
{
return _number;
}
protected override Expression VisitBinary(BinaryExpression node)
{
switch (node.NodeType)
{
case ExpressionType.Modulo:
case ExpressionType.Equal:
case ExpressionType.GreaterThanOrEqual:
case ExpressionType.LessThanOrEqual:
case ExpressionType.NotEqual:
case ExpressionType.GreaterThan:
case ExpressionType.LessThan:
case ExpressionType.And:
case ExpressionType.AndAlso:
case ExpressionType.Or:
case ExpressionType.OrElse:
_number++;
break;
}
return base.VisitBinary(node);
}
}
Użyjmy wiec tej klasy i zobaczmy rezultat.
Expression<Func<Person, Person, bool>> expression =
(x, y) => x.Age > y.Age || x.Age > 27 ;
InfoExpressionVisitor visitor = new InfoExpressionVisitor();
visitor.Visit(expression);
Console.WriteLine("Parameters ->");
foreach (var item in visitor.GetParameters())
{
Console.WriteLine("\t"+item.Name);
}
Console.WriteLine("Number of Logical operations : " + visitor.GetNumberOfLogicExpressions().ToString());
Tyle, jeśli chodzi o analizę wyrażeń drzewiastych.
W następnym wpisie będziemy generować wyrażenia lub dopisywać do istniejących dodatkowe polecenia.
A potem przejdziemy do Curring w C#.
Kod związany z właściwościami pochodzi z tego pytania StackOverflow.
http://stackoverflow.com/questions/671968/retrieving-property-name-from-lambda-expression