You are on page 1of 34

Árboles de expresiones en detalle

 19/06/2016
 Tiempo de lectura: 9 minutos
 Colaboradores
o

Anterior: Información general

Los árboles de expresiones son estructuras de datos que definen código. Se


basan en las mismas estructuras que usa un compilador para analizar el código
y generar el resultado compilado. A medida que vaya leyendo este tutorial,
observará cierta similitud entre los árboles de expresiones y los tipos usados en
las API de Roslyn para compilar analizadores y correcciones de código. (Los
analizadores y las correcciones de código son paquetes de NuGet que realizan
un análisis estático en código y pueden sugerir posibles correcciones para un
desarrollador). Los conceptos son similares y el resultado final es una estructura
de datos que permite examinar el código fuente de forma significativa. En
cambio, los árboles de expresiones se basan en un conjunto de clases y API
totalmente diferente a las API de Roslyn.

Veamos un ejemplo sencillo. Aquí tiene una línea de código:


C#Copiar

var sum = 1 + 2;

Si fuera a analizarlo como un árbol de expresión, el árbol contiene varios


nodos. El nodo más externo es una instrucción de declaración de variable con
asignación ( var sum = 1 + 2; ). Ese nodo exterior contiene varios nodos
secundarios: una declaración de variable, un operador de asignación y una
expresión que representa el lado derecho del signo igual. Esa expresión se
subdivide aún más en expresiones que representan la operación de suma, y los
operandos izquierdo y derecho de la suma.
Vamos a profundizar un poco más en las expresiones que constituyen el lado
derecho del signo igual. La expresión es 1 + 2 . Se trata de una expresión
binaria. Concretamente, es una expresión binaria de suma. Una expresión
binaria de suma tiene dos elementos secundarios, que representan los nodos
izquierdo y derecho de la expresión de suma. En este caso, ambos nodos son
expresiones constantes: el operando izquierdo es el valor 1 y el operando
derecho es el valor 2 .

Visualmente, toda la instrucción es un árbol: puede empezar en el nodo raíz y


desplazarse a cada uno de los nodos del árbol para ver el código que compone
la instrucción:

 Instrucción de declaración de variable con asignación ( var sum = 1 + 2; )


o Declaración de tipo de variable implícita ( var sum )
o Palabra clave var implícita ( var )
o Declaración de nombre de variable ( sum )
o Operador de asignación ( = )
o Expresión binaria de suma ( 1 + 2 )
o Operando izquierdo ( 1 )
o Operador de suma ( + )
o Operando derecho ( 2 )

Esto puede parecer complicado, pero resulta muy eficaz. Siguiendo el mismo
proceso, puede descomponer expresiones mucho más complicadas. Tomemos
esta expresión como ejemplo:
C#Copiar

var finalAnswer = this.SecretSauceFunction(


currentState.createInterimResult(),
currentState.createSecondValue(1, 2),
decisionServer.considerFinalOptions("hello")) +
MoreSecretSauce('A', DateTime.Now, true);

La expresión anterior también es una declaración de variable con una


asignación. En este caso, el lado derecho de la asignación es un árbol mucho
más complicado. No voy a descomponer esta expresión, pero tenga en cuenta
lo que podrían ser los distintos nodos. Hay llamadas de método que usan el
objeto actual como un receptor, una que tiene un receptor this explícito y otra
que no. Hay llamadas de método que usan otros objetos de receptor, así como
argumentos constantes de tipos diferentes. Y, por último, hay un operador
binario de suma. Según el tipo de valor devuelto
de SecretSauceFunction() o MoreSecretSauce() , ese operador binario de
suma puede ser una llamada de método a un operador de suma invalidado, que
se resuelva en una llamada de método estático al operador binario de suma
definido para una clase.

A pesar de esta aparente complejidad, la expresión anterior crea una estructura


de árbol por la que se puede navegar con tanta facilidad como en el primer
ejemplo. Puede seguir recorriendo los nodos secundarios para buscar nodos
hoja en la expresión. Los nodos primarios tendrán referencias a sus elementos
secundarios, y cada nodo tiene una propiedad que describe de qué tipo es.

La estructura de los árboles de expresiones es muy coherente. Una vez que


conozca los aspectos básicos, podrá entender incluso el código más complejo
cuando esté representado como un árbol de expresión. La elegancia de la
estructura de datos explica cómo el compilador de C# puede analizar los
programas de C# más complejos y crear resultados correctos a partir de código
fuente complicado.

Una vez que esté familiarizado con la estructura de los árboles de expresiones,
verá que los conocimientos que ha adquirido le permiten trabajar rápidamente
con muchos escenarios más avanzados. Los árboles de expresiones ofrecen
posibilidades increíbles.

Además de traducir algoritmos para ejecutarlos en otros entornos, se pueden


usar árboles de expresiones para que resulte más fácil escribir algoritmos que
inspeccionen el código antes de ejecutarlo. Puede escribir un método cuyos
argumentos sean expresiones y, luego, examinar esas expresiones antes de
ejecutar el código. El árbol de expresión es una representación completa del
código: puede ver los valores de cualquier subexpresión. Puede ver los nombres
de propiedad y método. Puede ver el valor de las expresiones
constantes. También puede convertir un árbol de expresión en un delegado
ejecutable y ejecutar el código.

Las API de los árboles de expresiones permiten crear árboles que representan
casi cualquier construcción de código válida. En cambio, para que todo resulte
lo más sencillo posible, algunas expresiones de C# no se pueden crear en un
árbol de expresión. Un ejemplo son las expresiones asincrónicas (mediante las
palabras clave async y await ). Si necesita algoritmos asincrónicos, tendría que
manipular los objetos Task directamente, en lugar de confiar en la
compatibilidad del compilador. Otro ejemplo es en la creación de
bucles. Normalmente, puede crearlos usando
bucles for , foreach , while o do . Como verá más adelante en esta serie, las
API de los árboles de expresiones admiten una expresión de bucle individual,
con expresiones break y continue que controlan la repetición del bucle.

Lo único lo que no se puede hacer es modificar un árbol de expresión. Los


árboles de expresiones son estructuras de datos inmutables. Si quiere mutar
(cambiar) un árbol de expresión, debe crear un nuevo árbol que sea una copia
del original, pero con los cambios que quiera.

Siguiente: Tipos de marco que admiten árboles de expresión

Tipos de marco que admiten árboles


de expresión
 19/06/2016
 Tiempo de lectura: 6 minutos
 Colaboradores
o

o
o

Anterior: Árboles de expresiones en detalle

Hay una amplia lista de clases en .NET Core Framework que funcionan con
árboles de expresiones.En System.Linq.Expressions puede ver la lista
completa. En lugar de analizarla al completo, vamos a explicar cómo se han
diseñado las clases del marco.

En el diseño del lenguaje, una expresión es un cuerpo de código que se evalúa y


devuelve un valor.Las expresiones pueden ser muy sencillas: la expresión
constante 1 devuelve el valor constante de 1. También pueden ser más
complicadas: la expresión (-B + Math.Sqrt(B*B - 4 * A * C)) / (2 *
A) devuelve una raíz de una ecuación cuadrática (en el caso en el que la
ecuación tenga una solución).
Todo empieza con System.Linq.Expression

Una de las complejidades de trabajar con árboles de expresiones es que


muchos tipos de expresiones distintos son válidos en muchos lugares de los
programas. Piense en una expresión de asignación. El lado derecho de una
asignación podría ser un valor constante, una variable, una expresión de
llamada de método u otros elementos. Esa flexibilidad del lenguaje significa que
puede encontrarse con muchos tipos de expresiones diferentes en cualquier
parte de los nodos de un árbol al atravesar un árbol de expresión. Por lo tanto,
lo más sencillo consiste en trabajar con el tipo de expresión base, siempre que
sea posible. En cambio, en ocasiones necesitará saber más. La clase de
expresión base contiene una propiedad NodeType para ello. Esta devuelve un
elemento ExpressionType , que es una enumeración de tipos de expresiones
posibles. Una vez que sepa el tipo del nodo, puede convertirlo en ese tipo y
realizar acciones específicas sabiendo el tipo del nodo de expresión. Puede
buscar determinados tipos de nodo y, luego, trabajar con las propiedades
específicas de ese tipo de expresión.

Por ejemplo, este código imprimirá el nombre de una variable para una
expresión de acceso a la variable. He seguido el procedimiento que consiste en
comprobar el tipo de nodo, convertirlo en una expresión de acceso a la variable
y después comprobar las propiedades del tipo de expresión específico:
C#Copiar

Expression<Func<int, int>> addFive = (num) => num + 5;

if (addFive.NodeType == ExpressionType.Lambda)
{
var lambdaExp = (LambdaExpression)addFive;

var parameter = lambdaExp.Parameters.First();

Console.WriteLine(parameter.Name);
Console.WriteLine(parameter.Type);
}

Crear árboles de expresiones

La clase System.Linq.Expression también contiene muchos métodos estáticos


para crear expresiones. Estos métodos crean un nodo de expresión al usar los
argumentos proporcionados para sus elementos secundarios. De esta manera,
se crea una expresión a partir de sus nodos hoja.Por ejemplo, este código
genera una expresión de agregar:
C#Copiar

// Addition is an add expression for "1 + 2"


var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var addition = Expression.Add(one, two);

En este sencillo ejemplo puede ver que hay muchos tipos implicados a la hora
de crear árboles de expresiones y trabajar con ellos. Esta complejidad resulta
necesaria para proporcionar las capacidades del vocabulario variado que ofrece
el lenguaje C#.
Navegar por las API

Hay tipos de nodos de expresión que se asignan a casi todos los elementos de
sintaxis del lenguaje C#. Cada tipo tiene métodos específicos para ese tipo de
elemento del lenguaje. Es mucha información como para recordarla toda. En
lugar de intentar memorizar todo, estas son las técnicas que uso para trabajar
con árboles de expresiones:

1. Fíjese en los miembros de la enumeración ExpressionType para determinar los


posibles nodos que debe examinar. Esto resulta muy útil cuando quiere atravesar
y comprender un árbol de expresión.
2. Fíjese en los miembros estáticos de la clase Expression para crear una
expresión. Esos métodos pueden crear cualquier tipo de expresión a partir de un
conjunto de sus nodos secundarios.
3. Fíjese en la clase ExpressionVisitor para crear un árbol de expresión
modificado.

Encontrará más información a medida que observe cada una de esas tres
áreas. Siempre encontrará lo que necesita empezando con uno de esos tres
pasos.

Siguiente: Ejecutar árboles de expresión

Ejecución de árboles de expresión


 19/06/2016
 Tiempo de lectura: 11 minutos
 Colaboradores
o
o

o
Anterior: Tipos de marco que admiten árboles de expresión

Un árbol de expresión es una estructura de datos que representa un código. No


es código compilado y ejecutable. Si quiere ejecutar el código de .NET que se
representa mediante un árbol de expresión, debe convertirlo en instrucciones
de lenguaje intermedio ejecutables.
Expresiones lambda a funciones

Puede convertir cualquier objeto LambdaExpression o cualquier tipo derivado


de LambdaExpression en lenguaje intermedio ejecutable. Otros tipos de
expresión no se pueden convertir directamente a código. Esta restricción tiene
poco efecto en la práctica. Las expresiones lambda son los únicos tipos de
expresiones que podría querer ejecutar mediante la conversión a lenguaje
intermedio (IL) ejecutable. (Piense lo que significaría ejecutar directamente
una ConstantExpression . ¿Tendría algún significado útil?). Cualquier árbol de
expresión que es una LambdaExpression o un tipo derivado
de LambdaExpression se puede convertir a lenguaje intermedio. El tipo de
expresión Expression<TDelegate> es el único ejemplo concreto en las
bibliotecas de .NET Core. Se usa para representar una expresión que se asigna a
cualquier tipo de delegado. Dado que este tipo se asigna a un tipo de delegado,
.NET puede examinar la expresión y generar el lenguaje intermedio de un
delegado adecuado que coincida con la firma de la expresión lambda.

En la mayoría de los casos, esto crea una asignación simple entre una expresión
y su delegado correspondiente. Por ejemplo, un árbol de expresión que se
representa por Expression<Func<int>> se convertiría a un delegado del
tipo Func<int> . Para una expresión lambda con cualquier tipo de valor
devuelto y lista de argumentos, existe un tipo de delegado que es el tipo de
destino para el código ejecutable representado por esa expresión lambda.

El tipo LambdaExpression contiene los


miembros Compile y CompileToMethod que se usarían para convertir un árbol
de expresión en código ejecutable. El método Compile crea un delegado. El
método CompileToMethod actualiza un objeto MethodBuilder con el lenguaje
intermedio que representa la salida compilada del árbol de expresión. Tenga en
cuenta que CompileToMethod solo está disponible en el marco de trabajo de
escritorio completo, no en .NET Core.

Como opción, también puede proporcionar un DebugInfoGenerator para que


reciba la información de depuración de símbolos para el objeto de delegado
generado. Esto le permite convertir el árbol de expresión en un objeto de
delegado y disponer de información de depuración completa sobre el delegado
generado.

Para convertir una expresión en un delegado se usaría el siguiente código:


C#Copiar

Expression<Func<int>> add = () => 1 + 2;


var func = add.Compile(); // Create Delegate
var answer = func(); // Invoke Delegate
Console.WriteLine(answer);

Observe que el tipo de delegado se basa en el tipo de expresión. Debe conocer


el tipo de valor devuelto y la lista de argumentos si quiere usar el objeto de
delegado de una forma fuertemente tipada. El
método LambdaExpression.Compile() devuelve el tipo Delegate . Tendrá que
convertirlo al tipo de delegado correcto para que las herramientas de tiempo de
compilación comprueben la lista de argumentos del tipo de valor devuelto.
Ejecución y duraciones

El código se ejecuta mediante la invocación del delegado que se crea al llamar


a LambdaExpression.Compile() . Puede verlo encima de
donde add.Compile() devuelve un delegado.Invocar ese delegado, mediante
una llamada a func() ejecuta el código.

Ese delegado representa el código en el árbol de expresión. Se puede conservar


el identificador a ese delegado e invocarlo más adelante. No es necesario
compilar el árbol de expresión cada vez que se quiera ejecutar el código que
representa. (Recuerde que los árboles de expresión son inmutables y que
compilar el mismo árbol de expresión más adelante creará un delegado que
ejecuta el mismo código).

No es aconsejable intentar crear cualquier mecanismo de almacenamiento en


caché más sofisticado para aumentar el rendimiento evitando llamadas
innecesarias de compilación. La comparación de dos árboles de expresión
arbitrarios para determinar si representan el mismo algoritmo también
consumirá mucho tiempo de ejecución. Probablemente comprobará que el
tiempo de proceso que se ahorra al evitar llamadas adicionales
a LambdaExpression.Compile() será consumido por el tiempo de ejecución de
código que determina que dos árboles de expresión diferentes devuelven el
mismo código ejecutable.
Advertencias

Compilar una expresión lambda en un delegado e invocar ese delegado es una


de las operaciones más simples que se pueden realizar con un árbol de
expresión. Pero incluso con esta sencilla operación, hay advertencias que debe
conocer.

Las expresiones lambda crean clausuras sobre las variables locales a las que se
hace referencia en la expresión. Debe garantizar que las variables que formarían
parte del delegado se pueden usar en la ubicación desde la que se llama
a Compile , y cuando se ejecuta el delegado resultante.

En general, el compilador se asegurará de que esto es cierto. Pero si la


expresión tiene acceso a una variable que implementa IDisposable , es posible
que el código deseche el objeto mientras se sigue manteniendo en el árbol de
expresión.

Por ejemplo, este código funciona bien porque int no


implementa IDisposable :
C#Copiar

private static Func<int, int> CreateBoundFunc()


{
var constant = 5; // constant is captured by the expression tree
Expression<Func<int, int>> expression = (b) => constant + b;
var rVal = expression.Compile();
return rVal;
}

El delegado capturó una referencia a la variable local constant . Esa variable es


accesible en cualquier momento posterior, cuando se ejecuta la función
devuelta por CreateBoundFunc .

Pero considere esta clase (bastante artificiosa) que implementa IDisposable :


C#Copiar

public class Resource : IDisposable


{
private bool isDisposed = false;
public int Argument
{
get
{
if (!isDisposed)
return 5;
else throw new ObjectDisposedException("Resource");
}
}

public void Dispose()


{
isDisposed = true;
}
}

Si se usa en una expresión como se muestra a continuación, obtendrá


una ObjectDisposedException al ejecutar el código al que hace referencia la
propiedad Resource.Argument :
C#Copiar

private static Func<int, int> CreateBoundResource()


{
using (var constant = new Resource()) // constant is captured by
the expression tree
{
Expression<Func<int, int>> expression = (b) =>
constant.Argument + b;
var rVal = expression.Compile();
return rVal;
}
}

El delegado devuelto por este método se clausuró sobre el objeto constant ,


que se eliminó. (Se eliminó porque se declaró en una instrucción using ).

Ahora, al ejecutar el delegado devuelto por este método, se producirá una


excepción ObjecctDisposedException en el punto de ejecución.

Parece extraño tener un error en tiempo de ejecución que representa una


construcción de tiempo de compilación, pero es el mundo al que se entra
cuando se trabaja con árboles de expresión.

Hay una gran cantidad de permutaciones de este problema, por lo que resulta
difícil ofrecer instrucciones generales para evitarlo. Tenga cuidado al obtener
acceso a las variables locales al definir expresiones y al obtener acceso al estado
en el objeto actual (representado por this ) al crear un árbol de expresión que
pueda ser devuelto por una API pública.

El código de la expresión puede hacer referencia a métodos o propiedades de


otros ensamblados.Ese ensamblado debe ser accesible cuando se define la
expresión, cuando se compila y cuando se invoca el delegado resultante. En los
casos en los que no esté presente, se producirá una
excepción ReferencedAssemblyNotFoundException .
Resumen

Los árboles de expresión que representan expresiones lambda se pueden


compilar para crear un delegado que se puede ejecutar. Esto proporciona un
mecanismo para ejecutar el código representado por un árbol de expresión.

El árbol de expresión representa el código que se ejecutaría para cualquier


construcción que se cree.Mientras que el entorno donde se compile y ejecute el
código coincida con el entorno donde se crea la expresión, todo funciona según
lo esperado. Cuando eso no sucede, los errores son muy predecibles y se
detectarán en las primeras pruebas de cualquier código que use los árboles de
expresión.

Interpretación de expresiones
 19/06/2016
 Tiempo de lectura: 20 minutos
 Colaboradores
o
o

o
Previous -- Executing Expressions (Anterior: Ejecución de expresiones)

Ahora, vamos a escribir código para examinar la estructura de un árbol de


expresión. Cada nodo de un árbol de expresión será un objeto de una clase
derivada de Expression .

Ese diseño hace que la visita de todos los nodos de un árbol de expresión sea
una operación recursiva relativamente sencilla. La estrategia general es
comenzar en el nodo raíz y determinar qué tipo de nodo es.

Si el tipo de nodo tiene elementos secundarios, visítelos recursivamente. En


cada nodo secundario, repita el proceso que ha usado en el nodo raíz:
determine el tipo, y si el tipo tiene elementos secundarios, visite cada uno de
ellos.
Examinar una expresión sin elementos secundarios

Comencemos visitando cada nodo en un árbol de expresión muy sencillo. Aquí


se muestra el código que crea una expresión constante y, después, examina sus
propiedades:
C#Copiar

var constant = Expression.Constant(24, typeof(int));

Console.WriteLine($"This is a/an {constant.NodeType} expression


type");
Console.WriteLine($"The type of the constant value is
{constant.Type}");
Console.WriteLine($"The value of the constant value is
{constant.Value}");

Esto imprimirá lo siguiente:


Copiar

This is an Constant expression type


The type of the constant value is System.Int32
The value of the constant value is 24

Ahora, vamos a escribir el código que examinará esta expresión y también


algunas propiedades importantes sobre este. Aquí se muestra el código:
Examinar una expresión de adición sencilla

Comencemos con el ejemplo de adición de la instrucción de esta sección.


C#Copiar
Expression<Func<int>> sum = () => 1 + 2;

No estoy usando var para declarar este árbol de expresión, y no es posible


porque el lado derecho de la asignación tiene un tipo implícito. Para
comprender esto mejor, lea aquí.

El nodo raíz es LambdaExpression . Para obtener el código interesante en el


lado derecho del operador => , necesita buscar uno de los elementos
secundarios de LambdaExpression . Haremos esto con todas las expresiones de
esta sección. El nodo primario nos ayuda a encontrar el tipo de valor devuelto
de LambdaExpression .

Para examinar cada nodo de esta expresión, necesitaremos visitar


recursivamente varios nodos. Aquí se muestra una primera implementación
sencilla:
C#Copiar

Expression<Func<int, int, int>> addition = (a, b) => a + b;

Console.WriteLine($"This expression is a {addition.NodeType}


expression type");
Console.WriteLine($"The name of the lambda is {((addition.Name ==
null) ? "<null>" : addition.Name)}");
Console.WriteLine($"The return type is
{addition.ReturnType.ToString()}");
Console.WriteLine($"The expression has {addition.Parameters.Count}
arguments. They are:");
foreach(var argumentExpression in addition.Parameters)
{
Console.WriteLine($"\tParameter Type:
{argumentExpression.Type.ToString()}, Name:
{argumentExpression.Name}");
}

var additionBody = (BinaryExpression)addition.Body;


Console.WriteLine($"The body is a {additionBody.NodeType}
expression");
Console.WriteLine($"The left side is a {additionBody.Left.NodeType}
expression");
var left = (ParameterExpression)additionBody.Left;
Console.WriteLine($"\tParameter Type: {left.Type.ToString()}, Name:
{left.Name}");
Console.WriteLine($"The right side is a {additionBody.Right.NodeType}
expression");
var right= (ParameterExpression)additionBody.Right;
Console.WriteLine($"\tParameter Type: {right.Type.ToString()}, Name:
{right.Name}");

Este ejemplo imprime el siguiente resultado:


Copiar

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
Parameter Type: System.Int32, Name: a
Parameter Type: System.Int32, Name: b
The body is a/an Add expression
The left side is a Parameter expression
Parameter Type: System.Int32, Name: a
The right side is a Parameter expression
Parameter Type: System.Int32, Name: b

Observará muchas repeticiones en el ejemplo de código anterior. Vamos a


limpiarlo y a crear un visitante del nodo de expresión con una finalidad más
general. Para ello vamos a necesitar escribir un algoritmo recursivo. Cualquier
nodo puede ser de un tipo que pueda tener elementos secundarios. Cualquier
nodo que tenga elementos secundarios necesita que los visitemos y
determinemos cuál es ese nodo. Aquí se muestra una versión limpia que usa la
recursividad para visitar las operaciones de adición:
C#Copiar

// Base Visitor class:


public abstract class Visitor
{
private readonly Expression node;

protected Visitor(Expression node)


{
this.node = node;
}

public abstract void Visit(string prefix);

public ExpressionType NodeType => this.node.NodeType;


public static Visitor CreateFromExpression(Expression node)
{
switch(node.NodeType)
{
case ExpressionType.Constant:
return new ConstantVisitor((ConstantExpression)node);
case ExpressionType.Lambda:
return new LambdaVisitor((LambdaExpression)node);
case ExpressionType.Parameter:
return new
ParameterVisitor((ParameterExpression)node);
case ExpressionType.Add:
return new BinaryVisitor((BinaryExpression)node);
default:
Console.Error.WriteLine($"Node not processed yet:
{node.NodeType}");
return default(Visitor);
}
}
}

// Lambda Visitor
public class LambdaVisitor : Visitor
{
private readonly LambdaExpression node;
public LambdaVisitor(LambdaExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This expression is a {NodeType}
expression type");
Console.WriteLine($"{prefix}The name of the lambda is
{((node.Name == null) ? "<null>" : node.Name)}");
Console.WriteLine($"{prefix}The return type is
{node.ReturnType.ToString()}");
Console.WriteLine($"{prefix}The expression has
{node.Parameters.Count} argument(s). They are:");
// Visit each parameter:
foreach (var argumentExpression in node.Parameters)
{
var argumentVisitor =
Visitor.CreateFromExpression(argumentExpression);
argumentVisitor.Visit(prefix + "\t");
}
Console.WriteLine($"{prefix}The expression body is:");
// Visit the body:
var bodyVisitor = Visitor.CreateFromExpression(node.Body);
bodyVisitor.Visit(prefix + "\t");
}
}

// Binary Expression Visitor:


public class BinaryVisitor : Visitor
{
private readonly BinaryExpression node;
public BinaryVisitor(BinaryExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This binary expression is a
{NodeType} expression");
var left = Visitor.CreateFromExpression(node.Left);
Console.WriteLine($"{prefix}The Left argument is:");
left.Visit(prefix + "\t");
var right = Visitor.CreateFromExpression(node.Right);
Console.WriteLine($"{prefix}The Right argument is:");
right.Visit(prefix + "\t");
}
}

// Parameter visitor:
public class ParameterVisitor : Visitor
{
private readonly ParameterExpression node;
public ParameterVisitor(ParameterExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This is an {NodeType} expression
type");
Console.WriteLine($"{prefix}Type: {node.Type.ToString()},
Name: {node.Name}, ByRef: {node.IsByRef}");
}
}

// Constant visitor:
public class ConstantVisitor : Visitor
{
private readonly ConstantExpression node;
public ConstantVisitor(ConstantExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This is an {NodeType} expression
type");
Console.WriteLine($"{prefix}The type of the constant value is
{node.Type}");
Console.WriteLine($"{prefix}The value of the constant value is
{node.Value}");
}
}

Este algoritmo es la base de un algoritmo que puede visitar


cualquier LambdaExpression arbitrario.Existen muchas vulnerabilidades,
concretamente que el código que he creado solo busca una muestra muy
pequeña de los posibles conjuntos de nodos de árbol de expresión que puede
encontrar. En cambio, todavía puede aprender bastante de lo que genera. (El
caso predeterminado en el método Visitor.CreateFromExpression imprime
un mensaje a la consola de error cuando se detecta un nuevo tipo de nodo. De
esta manera, sabe que se va a agregar un nuevo tipo de expresión).

Cuando ejecuta este visitante en la expresión de adición que se muestra arriba,


obtiene el siguiente resultado:
Copiar

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
Ahora que ha creado una implementación de visitante más general, puede
visitar y procesar muchos más tipos de expresiones diferentes.
Examinar una expresión de adición con muchos niveles

Vamos a probar un ejemplo más complicado, todavía limitando los tipos de


nodo solo a los de adición:
C#Copiar

Expression<Func<int>> sum = () => 1 + 2 + 3 + 4;

Antes de que ejecute esto en el algoritmo de visitante, haga un ejercicio de


reflexión para calcular cuál podría ser el resultado. Recuerde que el
operador + es un operador binario: debe tener dos elementos secundarios, que
representen los operandos izquierdo y derecho. Existen varias maneras posibles
de construir un árbol que pueda ser correcto:
C#Copiar

Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));


Expression<Func<int>> sum2 = () => ((1 + 2) + 3) + 4;

Expression<Func<int>> sum3 = () => (1 + 2) + (3 + 4);


Expression<Func<int>> sum4 = () => 1 + ((2 + 3) + 4);
Expression<Func<int>> sum5 = () => (1 + (2 + 3)) + 4;

Puede ver la separación en dos respuestas posibles para resaltar la más


prometedora. La primera representa las expresiones asociativas por la
derecha. La segunda representa las expresiones asociativas por la izquierda. La
ventaja de los dos formatos es que el formato escala a cualquier número
arbitrario de expresiones de adición.

Si ejecuta esta expresión a través del visitante, verá este resultado y comprobará
que la expresión de adición simple es asociativa por la izquierda.

Para ejecutar este ejemplo, y ver el árbol de expresión completo, tuve que
realizar un cambio en el árbol de expresión de origen. Cuando el árbol de
expresión contiene todas las constantes, el árbol resultante simplemente
contiene el valor constante de 10 . El compilador realiza toda la adición y
reduce la expresión a su forma más simple. Simplemente con agregar una
variable a la expresión es suficiente para ver el árbol original:
C#Copiar
Expression<Func<int, int>> sum = (a) => 1 + a + 3 + 4;

Cree un visitante para esta suma y ejecute el visitante para ver este resultado:
Copiar

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is
System.Int32
The value of the constant value is 1
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef:
False
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 3
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 4

También puede ejecutar cualquiera de los otros ejemplos a través del código de
visitante y ver qué árbol representa. Aquí se muestra un ejemplo de la
expresión sum3 anterior (con un parámetro adicional para evitar que el
compilador calcule la constante):
C#Copiar

Expression<Func<int, int, int>> sum3 = (a, b) => (1 + a) + (3 + b);


Aquí se muestra el resultado del visitante:
Copiar

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 2 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False
The expression body is:
This binary expression is a Add expression
The Left argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: a, ByRef: False
The Right argument is:
This binary expression is a Add expression
The Left argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 3
The Right argument is:
This is an Parameter expression type
Type: System.Int32, Name: b, ByRef: False

Tenga en cuenta que los paréntesis no forman parte del resultado. No existen
nodos en el árbol de expresión que representen los paréntesis en la expresión
de entrada. La estructura del árbol de expresión contiene toda la información
necesaria para comunicar la precedencia.
Ampliar a partir de este ejemplo

El ejemplo trata solo los árboles de expresión más básicos. El código que ha
visto en esta sección solo controla enteros constantes y el operador
binario + . Como último ejemplo, vamos a actualizar el visitante para que
controle una expresión más complicada. Hagamos que funcione para esto:
C#Copiar

Expression<Func<int, int>> factorial = (n) =>


n == 0 ?
1 :
Enumerable.Range(1, n).Aggregate((product, factor) => product *
factor);

Este código representa una posible implementación para la


función factorial matemática. La manera en que he escrito este código resalta
dos limitaciones en la creación de árboles de expresión asignando expresiones
lambda a las expresiones. En primer lugar, las expresiones lambda de
instrucción no están permitidas. Eso significa que no puedo usar bucles,
bloques, instrucciones IF/ELSE ni otras estructuras de control comunes en
C#. Estoy limitado al uso de expresiones. En segundo lugar, no puedo llamar
recursivamente a la misma expresión. Podría si ya fuera un delegado, pero no
puedo llamarla en su forma de árbol de expresión. En la sección de Crear
árboles de expresión, obtendrá las técnicas para superar estas limitaciones.

En esta expresión, encontrará nodos de todos estos tipos:

1. Equal (expresión binaria)


2. Multiply (expresión binaria)
3. Conditional (la expresión ? :)
4. Expresión de llamada a método (con la llamada a Range() y Aggregate() )

Una manera de modificar el algoritmo de visitante es seguir ejecutándolo y


escribir el tipo de nodo cada vez que llegue a su cláusula default . Después de
varias iteraciones, habrá visto cada uno de los nodos potenciales. Después, tiene
todo lo que necesita. El resultado será similar al siguiente:
C#Copiar

public static Visitor CreateFromExpression(Expression node)


{
switch(node.NodeType)
{
case ExpressionType.Constant:
return new ConstantVisitor((ConstantExpression)node);
case ExpressionType.Lambda:
return new LambdaVisitor((LambdaExpression)node);
case ExpressionType.Parameter:
return new ParameterVisitor((ParameterExpression)node);
case ExpressionType.Add:
case ExpressionType.Equal:
case ExpressionType.Multiply:
return new BinaryVisitor((BinaryExpression)node);
case ExpressionType.Conditional:
return new
ConditionalVisitor((ConditionalExpression)node);
case ExpressionType.Call:
return new MethodCallVisitor((MethodCallExpression)node);
default:
Console.Error.WriteLine($"Node not processed yet:
{node.NodeType}");
return default(Visitor);
}
}

ConditionalVisitor y MethodCallVisitor procesan esos dos nodos:


C#Copiar

public class ConditionalVisitor : Visitor


{
private readonly ConditionalExpression node;
public ConditionalVisitor(ConditionalExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This expression is a {NodeType}
expression");
var testVisitor = Visitor.CreateFromExpression(node.Test);
Console.WriteLine($"{prefix}The Test for this expression
is:");
testVisitor.Visit(prefix + "\t");
var trueVisitor = Visitor.CreateFromExpression(node.IfTrue);
Console.WriteLine($"{prefix}The True clause for this
expression is:");
trueVisitor.Visit(prefix + "\t");
var falseVisitor = Visitor.CreateFromExpression(node.IfFalse);
Console.WriteLine($"{prefix}The False clause for this
expression is:");
falseVisitor.Visit(prefix + "\t");
}
}

public class MethodCallVisitor : Visitor


{
private readonly MethodCallExpression node;
public MethodCallVisitor(MethodCallExpression node) : base(node)
{
this.node = node;
}

public override void Visit(string prefix)


{
Console.WriteLine($"{prefix}This expression is a {NodeType}
expression");
if (node.Object == null)
Console.WriteLine($"{prefix}This is a static method
call");
else
{
Console.WriteLine($"{prefix}The receiver (this) is:");
var receiverVisitor =
Visitor.CreateFromExpression(node.Object);
receiverVisitor.Visit(prefix + "\t");
}

var methodInfo = node.Method;


Console.WriteLine($"{prefix}The method name is
{methodInfo.DeclaringType}.{methodInfo.Name}");
// There is more here, like generic arguments, and so on.
Console.WriteLine($"{prefix}The Arguments are:");
foreach(var arg in node.Arguments)
{
var argVisitor = Visitor.CreateFromExpression(arg);
argVisitor.Visit(prefix + "\t");
}
}
}

Y el resultado del árbol de expresión será:


Copiar

This expression is a/an Lambda expression type


The name of the lambda is <null>
The return type is System.Int32
The expression has 1 argument(s). They are:
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
The expression body is:
This expression is a Conditional expression
The Test for this expression is:
This binary expression is a Equal expression
The Left argument is:
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef: False
The Right argument is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 0
The True clause for this expression is:
This is an Constant expression type
The type of the constant value is System.Int32
The value of the constant value is 1
The False clause for this expression is:
This expression is a Call expression
This is a static method call
The method name is System.Linq.Enumerable.Aggregate
The Arguments are:
This expression is a Call expression
This is a static method call
The method name is
System.Linq.Enumerable.Range
The Arguments are:
This is an Constant expression type
The type of the constant value is
System.Int32
The value of the constant value is 1
This is an Parameter expression type
Type: System.Int32, Name: n, ByRef:
False
This expression is a Lambda expression type
The name of the lambda is <null>
The return type is System.Int32
The expression has 2 arguments. They are:
This is an Parameter expression type
Type: System.Int32, Name: product,
ByRef: False
This is an Parameter expression type
Type: System.Int32, Name: factor,
ByRef: False
The expression body is:
This binary expression is a Multiply
expression
The Left argument is:
This is an Parameter
expression type
Type: System.Int32, Name:
product, ByRef: False
The Right argument is:
This is an Parameter
expression type
Type: System.Int32, Name:
factor, ByRef: False

Ampliar la biblioteca de ejemplo

Los ejemplos de esta sección muestran las técnicas principales para visitar y
examinar nodos de un árbol de expresión. He pasado por alto muchas acciones
que puede necesitar para concentrarme en las tareas principales a la hora de
visitar y acceder a los nodos de un árbol de expresión.

En primer lugar, los visitantes solo controlan constantes que son enteros. Los
valores constantes pueden ser cualquier otro tipo numérico, y el lenguaje de C#
admite conversiones y promociones entre esos tipos. Una versión más sólida de
este código reflejará todas esas capacidades.

Incluso en el último ejemplo se reconoce un subconjunto de los tipos de nodo


posibles. Todavía puede proporcionarle muchas expresiones que provocarán
que se produzca un error. Se incluye una implementación completa en .NET
Standard bajo el nombre ExpressionVisitor y puede controlar todos los tipos de
nodo posibles.

Por último, la biblioteca que he usado en este artículo se ha creado con fines de
demostración y aprendizaje. No está optimizada. La he escrito para dejar claro
las estructuras que he usado y para resaltar las técnicas usadas para visitar los
nodos y analizar lo que hay ahí. Una implementación de producción prestaría
más atención al rendimiento.

Incluso con esas limitaciones, se encuentra en el camino correcto para escribir


algoritmos que lean y comprendan árboles de expresión.

Next -- Building Expressions (Siguiente: Creación de expresiones)

Crear árboles de expresión


 19/06/2016
 Tiempo de lectura: 9 minutos
 Colaboradores
o
o

o
Previous -- Interpreting Expressions (Anterior: Interpretación de expresiones)

Hasta ahora, todos los árboles de expresión que ha visto se han creado con el
compilador de C#.Todo lo que tenía que hacer era crear una expresión lambda
que se asignaba a una variable de tipo Expression<Func<T>> o de algún tipo
similar. Esa no es la única manera de crear un árbol de expresión. En muchos
escenarios, puede que necesite crear una expresión en memoria en tiempo de
ejecución.

Crear árboles de expresión es complicado por el hecho de que esos árboles de


expresión son inmutables. Inmutable significa que debe crear el árbol desde las
hojas hasta la raíz. Las API que usará para crear los árboles de expresión reflejan
este hecho: los métodos que usará para crear un nodo toman todos sus
elementos secundarios como argumentos. Veamos algunos ejemplos para
mostrarle las técnicas.
Crear nodos

Comencemos por algo relativamente sencillo de nuevo. Usaremos la expresión


de adición con la que he estado trabajando en estas secciones:
C#Copiar

Expression<Func<int>> sum = () => 1 + 2;

Para crear ese árbol de expresión, debe crear los nodos de hoja. Los nodos de
hoja son constantes, por lo que puede usar el
método Expression.Constant para crear los nodos:
C#Copiar

var one = Expression.Constant(1, typeof(int));


var two = Expression.Constant(2, typeof(int));

Después, creará la expresión de adición:


C#Copiar

var addition = Expression.Add(one, two);

Una vez que tenga la expresión de adición, puede crear la expresión lambda:
C#Copiar

var lambda = Expression.Lambda(addition);


Esta es una expresión lambda muy sencilla porque no contiene
argumentos. Posteriormente en esta sección, verá cómo asignar argumentos a
parámetros y crear expresiones más complicadas.

Para las expresiones que son tan simples como esta, puede combinar todas las
llamadas en una sola instrucción:
C#Copiar

var lambda = Expression.Lambda(


Expression.Add(
Expression.Constant(1, typeof(int)),
Expression.Constant(2, typeof(int))
)
);

Crear un árbol

Estos son los aspectos básicos para crear un árbol de expresión en la


memoria. Los árboles más complejos implican normalmente más tipos de nodo
y más nodos en el árbol. Vamos a analizar un ejemplo más y a mostrar dos tipos
de nodo que creará normalmente al crear árboles de expresión: los nodos de
argumentos y los nodos de llamada al método.

Vamos a crear un árbol de expresión para crear esta expresión:


C#Copiar

Expression<Func<double, double, double>> distanceCalc =


(x, y) => Math.Sqrt(x * x + y * y);

Comenzará creando expresiones de parámetro para x y y :


C#Copiar

var xParameter = Expression.Parameter(typeof(double), "x");


var yParameter = Expression.Parameter(typeof(double), "y");

La creación de expresiones de adición y multiplicación sigue el patrón que ya ha


visto:
C#Copiar

var xSquared = Expression.Multiply(xParameter, xParameter);


var ySquared = Expression.Multiply(yParameter, yParameter);
var sum = Expression.Add(xSquared, ySquared);
Después, necesita crear una expresión de llamada al método para la llamada
a Math.Sqrt .
C#Copiar

var sqrtMethod = typeof(Math).GetMethod("Sqrt", new[] { typeof(double)


});
var distance = Expression.Call(sqrtMethod, sum);

Y, por último, coloque la llamada al método en una expresión lambda y


asegúrese de definir los argumentos en dicha expresión:
C#Copiar

var distanceLambda = Expression.Lambda(


distance,
xParameter,
yParameter);

En este ejemplo más complejo, verá un par de técnicas más que necesitará a
menudo para crear árboles de expresión.

Primero, necesita crear los objetos que representan parámetros o variables


locales antes de usarlos.Una vez que haya creado esos objetos, puede usarlos
en su árbol de expresión siempre que los necesite.

Después, necesita usar un subconjunto de las API de reflexión para crear un


objeto MethodInfo , de manera que pueda crear un árbol de expresión para
tener acceso a ese método. Debe limitarse al subconjunto de las API de
reflexión que están disponibles en la plataforma de .NET Core. De nuevo, estas
técnicas se extenderán a otros árboles de expresión.
Crear código en profundidad

No está limitado en lo que puede crear con estas API. En cambio, cuánto más
complicado sea el árbol de expresión que quiera crear, más difícil será la
administración y la lectura del código.

Vamos a crear un árbol de expresión que sea equivalente a este código:


C#Copiar

Func<int, int> factorialFunc = (n) =>


{
var res = 1;
while (n > 1)
{
res = res * n;
n--;
}
return res;
};

Observe anteriormente que no he creado el árbol de expresión, sino


simplemente el delegado. Con la clase Expression no puede crear expresiones
lambda de instrucción. Aquí se muestra el código necesario para crear la misma
función. Es complicado por el hecho de que no existe una API para crear un
bucle while , en su lugar necesita crear un bucle que contenga una prueba
condicional y un destino de la etiqueta para salir del bucle.
C#Copiar

var nArgument = Expression.Parameter(typeof(int), "n");


var result = Expression.Variable(typeof(int), "result");

// Creating a label that represents the return value


LabelTarget label = Expression.Label(typeof(int));

var initializeResult = Expression.Assign(result,


Expression.Constant(1));

// This is the inner block that performs the multiplication,


// and decrements the value of 'n'
var block = Expression.Block(
Expression.Assign(result,
Expression.Multiply(result, nArgument)),
Expression.PostDecrementAssign(nArgument)
);

// Creating a method body.


BlockExpression body = Expression.Block(
new[] { result },
initializeResult,
Expression.Loop(
Expression.IfThenElse(
Expression.GreaterThan(nArgument, Expression.Constant(1)),
block,
Expression.Break(label, result)
),
label
)
);

El código para crear el árbol de expresión para la función factorial es bastante


más largo, más complicado y está lleno de etiquetas, instrucciones Break y otros
elementos que nos gustaría evitar en nuestras tareas de codificación diarias.

En esta sección, también he actualizado el código del visitante para visitar cada
nodo de este árbol de expresión y escribir información sobre los nodos que se
crean en este ejemplo. Puede ver o descargar el código de ejemplo en el
repositorio dotnet/docs de GitHub. Pruébelo compilando y ejecutando los
ejemplos. Para obtener instrucciones de descarga, vea Ejemplos y tutoriales.
Examinar las API

Las API del árbol de expresión son algunas de las más difíciles para navegar en
.NET Core, pero no pasa nada. Su finalidad es una tarea más compleja: escribir
código que genere código en tiempo de ejecución. Son inevitablemente
complicadas a la hora de proporcionar un equilibrio entre la compatibilidad con
todas las estructuras de control disponibles en el lenguaje de C# y mantener el
área expuesta de las API tan pequeña como sea razonable. Este equilibrio
significa que muchas estructuras de control se representan no por sus
construcciones de C#, sino por construcciones que representan la lógica
subyacente que genera el compilador desde estas construcciones de nivel
superior.

Además, en este momento, existen expresiones de C# que no pueden crearse


directamente con métodos de clase Expression . En general, estos serán las
expresiones y los operadores más recientes que se han agregado a C# 5 y C#
6. (Por ejemplo, las expresiones async no pueden crearse y el
operador ?. nuevo no puede crearse directamente).

Next -- Translating Expressions (Siguiente: Traducción de expresiones)

https://docs.microsoft.com/es-es/dotnet/csharp/expression-trees-building

You might also like