Septiembre de 2019
Volumen 34, número 9
[Desarrollo para .NET]
Árboles de expresión en Visual Basic y C#
Por Zev Spitz
Imagine que tuviera objetos que representaran partes de un programa. El código podría combinar esas partes de varias formas y crear un objeto que representase un programa nuevo. Podría compilar este objeto en un nuevo programa y ejecutarlo. El programa podría, incluso, reescribir su propia lógica. Podría pasar este objeto a otro entorno, donde se analizarían las partes individuales como un conjunto de instrucciones que deberían ejecutarse en este otro entorno.
Bienvenido a los árboles de expresión. El objetivo de este artículo es proporcionar información general sobre los árboles de expresión, su uso en el modelado de operaciones de código y en la compilación de código en tiempo de ejecución, y también de cómo crearlos y transformarlos.
Una nota sobre el lenguaje. Para muchas de las características de lenguaje de Visual Basic y C#, la sintaxis es solo un contenedor fino de métodos y tipos de .NET que se pueden usar con cualquier lenguaje. Por ejemplo, el bucle foreach de C# y la construcción For Each de Visual Basic llaman al método GetEnumerator del tipo de implementación IEnumerable. La sintaxis de la palabra clave LINQ se resuelve en métodos como Select y Where con la signatura correspondiente. Cada vez que escribe Using en Visual Basic o un bloque using en C#, se genera una llamada a IDisposable.Dispose encapsulado en el control de errores.
Así ocurre también con los árboles de expresión. Tanto la sintaxis de Visual Basic como la de C# permiten la creación de árboles de expresión y los dos pueden usar la infraestructura de .NET (en el espacio de nombres System.Linq.Expressions) para trabajar con ellos. Por tanto, los conceptos que aquí se explican se aplican por igual a ambos lenguajes. Tanto si su principal lenguaje de desarrollo es C# como si es Visual Basic, creo que este artículo le resultará de gran utilidad.
Expresiones, árboles de expresión y los tipos de System.Linq.Expressions
Una expresión en Visual Basic y C# es un fragmento de código que devuelve un valor cuando se evalúa, por ejemplo:
42
"abcd"
True
n
Las expresiones pueden estar formadas por otras expresiones, por ejemplo:
x + y
"abcd".Length < 5 * 2
Estas forman un árbol de expresiones o un árbol de expresión. Imagine n + 42 = 27 en Visual Basic o n + 42 == 27 en C#, que se pueden separar en dos partes, como se muestra en la figura 1.
Figura 1. Desglose de un árbol de expresión
.NET proporciona una serie de tipos en el espacio de nombres System.Linq.Expressions para construir estructuras de datos que representen árboles de expresión. Por ejemplo, n + 42 = 27 se puede representar con objetos y propiedades, como se muestra en la figura 2. Tenga en cuenta que este código no se compilará. Estos tipos no tienen constructores públicos y son inmutables; por tanto, no hay inicializadores de objetos.
Figura 2. Objetos de un árbol de expresión con notación de objetos
New BinaryExpression With {
.NodeType = ExpressionType.Equal,
.Left = New BinaryExpression With {
.NodeType = ExpressionType.Add,
.Left = New ParameterExpression With {
.Name = "n"
},
.Right = New ConstantExpression With {
.Value = 42
}
},
.Right = New ConstantExpression With {
.Value = 27
}
}
new BinaryExpression {
NodeType = ExpressionType.Equal,
Left = new BinaryExpression {
NodeType = ExpressionType.Add,
Left = new ParameterExpression {
Name = "n"
},
Right = new ConstantExpression {
Value = 42
}
},
Right = new ConstantExpression {
Value = 27
}
}
El término “árbol de expresión” en .NET se utiliza tanto para árboles de expresión sintáctica (x + y) como para objetos de un árbol de expresión (una instancia BinaryExpression).
Para comprender lo que representa un nodo determinado en el árbol de expresión, el tipo del objeto de nodo (BinaryExpression, ConstantExpression, ParameterExpression) solo describe la forma de la expresión. Por ejemplo, el tipo BinaryExpression indica que la expresión representada tiene dos operandos, pero no qué operación combina los operandos. ¿Los operandos se suman, se multiplican o se comparan numéricamente? Esa información se encuentra en la propiedad NodeType, definida en la clase base Expression. Tenga en cuenta que algunos tipos de nodos no representan operaciones de código. Vea “Tipos de metanodos”.
Tipos de metanodos
La mayoría de los tipos de nodos representan operaciones de código. Sin embargo, hay tres tipos de “metanodos”, es decir, tipos de nodos que proporcionan información sobre el árbol pero que no se asignan directamente al código.
ExpressionType.Quote. Este tipo de nodo siempre encapsula una expresión lambda y especifica que la expresión lambda define un nuevo árbol de expresión, no un delegado. Por ejemplo, el árbol que se genera con el siguiente código de Visual Basic:
Dim expr As Expression(Of Func(Of Func(Of Boolean))) = Function() Function() True
o con el siguiente código de C#:
Expression<Func<Func<bool>>> expr = () => () => true;
representa un delegado que produce otro delegado. Si quiere representar un delegado que produzca otro árbol de expresión, tiene que encapsular el elemento interno en un nodo Quote. El compilador lo hace automáticamente con el siguiente código de Visual Basic:
Dim expr As Expression(Of Func(Of Expression(Of Func(Of Boolean)))) =
Function() Function() True
O con el siguiente código de C#:
Expression<Func<Expression<Func<bool>>>> expr = () => () => true;
ExpressionType.DebugInfo. Este nodo emite información de depuración, de modo que, cuando depura expresiones compiladas, se puede asignar el código IL de un punto concreto al lugar adecuado en el código fuente.
ExpressionType.RuntimeVariablesExpression. Piense en el objeto arguments de ES3. Está disponible dentro de una función sin haberlo declarado como una variable explícita. Python expone una función locals, que devuelve un diccionario de variables definidas en el espacio de nombres local. Estas variables “virtuales” se describen en un árbol de expresión con el elemento RuntimeVariablesExpression.
Otra propiedad importante de Expression es Type. Tanto en Visual Basic como en C#, cada expresión tiene un tipo. Si se agregan dos tipos Integer, se producirá un Integer, mientras que dos tipos Double producirán un Double. La propiedad Type devuelve el valor de System.Type de la expresión.
Cuando se visualiza la estructura de un árbol de expresión, resulta útil centrarse en estas dos propiedades, como se muestra en la figura 3.
Figura 3. Propiedades NodeType, Type, Value y Name
Si no hay constructores públicos para estos tipos, ¿cómo se crean? Tiene dos posibilidades. El compilador puede generarlos automáticamente si escribe sintaxis lambda donde se espera un tipo de expresión. O bien puede usar métodos Shared (estáticos en C#) del patrón de diseño Factory Method en la clase Expression.
Construcción de objetos de un árbol de expresión I: usando el compilador
La forma más sencilla de construir estos objetos es dejar que los genere el compilador, como ya he comentado, usando sintaxis lambda donde se espera Expression(Of TDelegate) (Expression<TDelegate> en C#), ya sea asignados a una variable con tipo o como argumentos de un parámetro con tipo. En Visual Basic, la sintaxis lambda se especifica con las palabras clave Function o Sub, seguidas de una lista de parámetros y el cuerpo de un método. En C#, la sintaxis lambda se indica con el operador =>.
La sintaxis lambda que define un árbol de expresión se denomina lambda de expresión (a diferencia de una lambda de instrucción) y tiene este aspecto en C#:
Expression<Func<Integer, String>> expr = i => i.ToString();
IQueryable<Person> personSource = ...
var qry = qry.Select(x => x.LastName);
Y este en Visual Basic:
Dim expr As Expression(Of Func(Of Integer, String)) = Function(i) i.ToString
Dim personSource As IQueryable(Of Person) = ...
Dim qry = qry.Select(Function(x) x.LastName)
La creación de un árbol de expresión de esta forma tiene algunas limitaciones:
- Está limitado por la sintaxis lambda.
- Solo admite expresiones lambda de una línea, sin lambdas de varias líneas.
- Las lambdas de expresión solo pueden contener expresiones, no instrucciones, como If…Then o Try…Catch.
- No admite enlaces en tiempo de ejecución (o dinámicos en C#).
- En C#, no se admiten argumentos con nombre ni argumentos opcionales omitidos.
- No admite el operador de propagación nula.
La estructura completa del árbol de expresión construido (es decir, los tipos de nodos y de subexpresiones, y la resolución de sobrecarga de los métodos) se determina en tiempo de compilación. Puesto que los árboles de expresión son inmutables, la estructura no se puede modificar una vez creada.
Tenga en cuenta también que los árboles generados por el compilador imitan el comportamiento del compilador; por tanto, el árbol generado podría ser diferente de lo que había previsto con el código original (figura 4). Algunos ejemplos:
Figura 4. Árboles generados por el compilador frente al código fuente
Variables cubiertas. Para hacer referencia al valor actual de las variables al entrar y salir de las expresiones lambda, el compilador crea una clase oculta cuyos miembros corresponden a cada una de las variables a las que se hace referencia. La función de la expresión lambda se convierte en un método de la clase (vea “Variables cubiertas”). El árbol de expresión correspondiente representará las variables cubiertas como nodos MemberAccess en la instancia oculta. En Visual Basic, se anexa también el prefijo $VB$Local_ al nombre de la variable.
Variables cubiertas
Cuando una expresión lambda hace referencia a una variable definida fuera de ella, se dice que la expresión lambda “cubre” la variable. El compilador tiene que prestar especial atención a esta variable, porque la expresión lambda podría usarse después de cambiar el valor de la variable y se espera que la expresión lambda haga referencia al valor nuevo. O al revés, la expresión lambda podría cambiar el valor y ese cambio debería estar visible fuera de la expresión lambda. Por ejemplo, en el siguiente código de C#:
var i = 5;
Action lmbd = () => Console.WriteLine(i);
i = 6;
lmbd();
o en el siguiente código de Visual Basic:
Dim i = 5
Dim lmbd = Sub() Console.WriteLine(i)
i = 6
lmbd()
la salida que se espera sería 6, no 5, porque la expresión lambda debería usar su valor en el momento en el que se invoca, después de haber establecido i en 6.
El compilador de C# hace esto creando una clase oculta con las variables necesarias como campos de la clase y las expresiones lambda como métodos de la clase. Después, el compilador reemplaza todas las referencias a esa variable con acceso de miembro en la instancia de la clase. La instancia de la clase no cambia, pero el valor de sus campos sí puede cambiar. El código IL resultante sería similar al siguiente en C#:
[CompilerGenerated]
private sealed class <>c__DisplayClass0_0 {
public int i;
internal void <Main>b__0() => Console.WriteLine(i);
}
var @object = new <>c__DisplayClass0_0();
@object.i = 5;
Action lmbd = @object.<Main>b__0;
@object.i = 6;
lmbd();
El compilador de Visual Basic hace algo parecido, con una diferencia: se antepone $VB$Local al nombre de la propiedad, así:
<CompilerGenerated> Friend NotInheritable Class _Closure$__0-0
Public $VB$Local_i As Integer
Sub _Lambda$__0()
Console.WriteLine($VB$Local_i)
End Sub
End Class
Dim targetObject = New _Closure$__0-0 targetObject With { .$VB$Local_i = 5 }
Dim lmbd = AddressOf targetObject. _Lambda$__0
targetObject.i = 6
lmbd()
Operador NameOf. El resultado del operador NameOf se representa como un valor de cadena Constant.
Interpolación de cadenas y conversión boxing. Esto se resuelve como una llamada a String.Format y una cadena de formato Constant. Puesto que la expresión de interpolación es un tipo de valor (Date, el alias de Visual Basic para DateTime), mientras que el parámetro correspondiente de String.Format espera Object, el compilador encapsula también la expresión de interpolación con un nodo Convert como Object.
Llamadas a métodos de extensión. En realidad, son llamadas a métodos a nivel de módulo (métodos estáticos en C#) y se representan como tales en los árboles de expresión. Los métodos a nivel de módulo y Shared no tienen una instancia, por lo que la propiedad Object del elemento MethodCallExpression correspondiente devolverá Nothing (o null). Si MethodCallExpression representa una llamada a un método de instancia, la propiedad Object no será Nothing.
La forma en la que se representan los métodos de extensión en los árboles de expresión pone de relieve una diferencia importante entre los árboles de expresión y los árboles de sintaxis del compilador Roslyn, que mantienen la sintaxis tal cual. Los árboles de expresión se centran menos en la sintaxis precisa y más en las operaciones subyacentes. El árbol de sintaxis de Roslyn para una llamada a un método de extensión sería igual que una llamada a un método de instancia normal, no una llamada a un método estático o Shared.
Conversiones. Cuando el tipo de una expresión no coincide con el tipo esperado y hay una conversión implícita del tipo de la expresión, el compilador encapsula la expresión interna en un nodo Convert con el tipo esperado. El compilador de Visual Basic hace lo mismo cuando genera árboles de expresión con una expresión cuyo tipo implementa o se hereda del tipo esperado. Por ejemplo, en la figura 4, el método de extensión Count espera IEnumerable(Of Char), pero el tipo real de la expresión es String.
Construcción de objetos de un árbol de expresión II: usando métodos del patrón de diseño Factory Method
También puede crear árboles de expresión usando métodos Shared (estáticos en C#) del patrón de diseño Factory Method en System.Linq.Expressions.Expression. Por ejemplo, para construir los objetos del árbol de expresión para i.ToString, donde i es Integer en Visual Basic (o int en C#), use código como el siguiente:
' Imports System.Linq.Expressions.Expression
Dim prm As ParameterExpression = Parameter(GetType(Integer), "i")
Dim expr As Expression = [Call](
prm,
GetType(Integer).GetMethods("ToString", {})
)
En C#, el código debería ser como este:
// Using static System.Linq.Expressions.Expression
ParameterExpression prm = Parameter(typeof(int), "i");
Expression expr = Call(
prm,
typeof(int).GetMethods("ToString", new [] {})
);
Aunque crear árboles de expresión de esta manera suele requerir una cantidad considerable de reflexión y varias llamadas a los métodos del patrón de diseño Factory Method, tiene más flexibilidad para personalizar el árbol de expresión exacto que necesita. Con la sintaxis del compilador, tendría que escribir todas las variaciones posibles del árbol de expresión que el programa podría necesitar.
Además, la API de Factory Method permite compilar algunos tipos de expresiones que no se admiten actualmente en expresiones generadas por el compilador. Algunos ejemplos:
Instrucciones como System.Void, que devuelven ConditionalExpression, que corresponde a If..Then e If..Then..Else..End If (en C#, if (...) { ...} else { ... }); o TryCatchExpression representando un bloque Try..Catch o try { ... } catch (...) { ... }.
Assignments Dim x As Integer: x = 17.
Bloques para agrupar varias instrucciones.
Por ejemplo, imagine el siguiente código de Visual Basic:
Dim msg As String = "Hello!"
If DateTime.Now.Hour > 18 Then msg = "Good night"
Console.WriteLine(msg)
o el siguiente código de C# equivalente:
string msg = "Hello";
if (DateTime.Now.Hour > 18) {
msg = "Good night";
}
Console.WriteLine(msg);
Puede crear un árbol de expresión correspondiente en Visual Basic o en C# usando los métodos del patrón de diseño Factory Method, como se muestra en la figura 5.
Figura 5. Bloques, asignaciones e instrucciones en los árboles de expresión
' Imports System.Linq.Expressions.Expression
Dim msg = Parameter(GetType(String), "msg")
Dim body = Block(
Assign(msg, Constant("Hello")),
IfThen(
GreaterThan(
MakeMemberAccess(
MakeMemberAccess(
Nothing,
GetType(DateTime).GetMember("Now").Single
),
GetType(DateTime).GetMember("Hour").Single
),
Constant(18)
),
Assign(msg, Constant("Good night"))
),
[Call](
GetType(Console).GetMethod("WriteLine", { GetType(string) }),
msg
)
)
// Using static System.Linq.Expressions.Expression
var msg = Parameter(typeof(string), "msg");
var expr = Lambda(
Block(
Assign(msg, Constant("Hello")),
IfThen(
GreaterThan(
MakeMemberAccess(
MakeMemberAccess(
null,
typeof(DateTime).GetMember("Now").Single()
),
typeof(DateTime).GetMember("Hour").Single()
),
Constant(18)
),
Assign(msg, Constant("Good night"))
),
Call(
typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }),
msg
)
)
);
Uso de los árboles de expresión I: asignación de construcciones de código a API externas
En un principio, los árboles de expresión se diseñaron para habilitar la asignación de sintaxis de Visual Basic o C# a una API diferente. El caso de uso clásico es la generación de una instrucción SQL, como esta:
SELECT * FROM Persons WHERE Persons.LastName LIKE N'D%'
Esta instrucción podría derivarse de código como el del siguiente fragmento, que utiliza el acceso de miembro (operador .), llamadas a métodos dentro de la expresión y el método Queryable.Where. Este es el código en Visual Basic:
Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.LastName.StartsWith("D"))
y este es el código en C#:
IQueryable<Person> personSource = ...
var qry = personSource.Where(x => x.LastName.StartsWith("D");
¿Cómo funciona? Hay dos sobrecargas que se podrían usar con la expresión lambda: Enumerable.Where y Queryable.Where. Sin embargo, la resolución de sobrecargas prefiere la sobrecarga en la que la expresión lambda es una lambda de expresión (es decir, Queryable.Where) a la sobrecarga que toma un delegado. A continuación, el compilador reemplaza la sintaxis lambda con llamadas a los métodos del patrón de diseño Factory Method adecuados.
En tiempo de ejecución, el método Queryable.Where encapsula el árbol de expresión pasado con un nodo Call cuya propiedad Method hace referencia a Queryable.Where y que toma dos parámetros: personSource y el árbol de expresión de la sintaxis lambda (figura 6). El nodo Quote indica que el árbol de expresión interno se pasa a Queryable.Where como un árbol de expresión y no como un delegado.
Figura 6. Visualización del árbol de expresión final
Un proveedor de base de datos LINQ (como Entity Framework, LINQ2SQL o NHibernate) puede tomar este tipo de árbol de expresión y asignar las distintas partes a la instrucción SQL del principio de esta sección. Del siguiente modo:
- La llamada ExpresssionType.Call a Queryable.Where se analiza como una cláusula WHERE de SQL.
- El tipo ExpressionType.MemberAccess de LastName en una instancia de Person lee el campo LastName de la tabla Persons (Persons.LastName).
- La llamada ExpressionType.Call al método StartsWith con un argumento Constant se convierte en el operador LIKE de SQL, respecto a un patrón que coincide con el principio de una cadena de constante:
LIKE N'D%'
Por tanto, se pueden controlar las API externas con construcciones y convenciones de código, con todas las ventajas del compilador: seguridad de tipos y sintaxis correcta en las distintas partes del árbol de expresión y finalización automática en el IDE. Otros ejemplos:
Creación de solicitudes web. La biblioteca Simple.OData.Client (bit.ly/2YyDrsx) permite crear solicitudes OData pasando árboles de expresión a los distintos métodos. La biblioteca genera la solicitud correcta (la figura 7 muestra el código para Visual Basic y C#).
Figura 7. Solicitudes con la biblioteca Simple.OData.Client y árboles de expresión
Dim client = New ODataClient("https://services.odata.org/v4/TripPinServiceRW/")
Dim people = Await client.For(Of People)
.Filter(Function(x) x.Trips.Any(Function(y) y.Budget > 3000))
.Top(2)
.Select(Function(x) New With { x.FirstName, x.LastName})
.FindEntriesAsync
var client = new ODataClient("https://services.odata.org/v4/TripPinServiceRW/");
var people = await client.For<People>()
.Filter(x => x.Trips.Any(y => y.Budget > 3000))
.Top(2)
.Select(x => new {x.FirstName, x.LastName})
.FindEntriesAsync();
Solicitud saliente:
> https://services.odata.org/v4/TripPinServiceRW/People?$top=2 &
$select=FirstName, LastName & $filter=Trips/any(d:d/Budget gt 3000)
Reflexión por ejemplo. En lugar de usar reflexión para obtener un elemento MethodInfo, puede escribir una llamada al método dentro de una expresión y pasar esa expresión a una función que extraiga la sobrecarga específica utilizada en la llamada. Este es el código de reflexión en Visual Basic:
Dim writeLine as MethodInfo = GetType(Console).GetMethod(
"WriteLine", { GetType(String) })
Y el mismo código en C#:
MethodInfo writeLine = typeof(Console).GetMethod(
"WriteLine", new [] { typeof(string) });
Esta función y su uso en Visual Basic podrían tener este aspecto:
Function GetMethod(expr As Expression(Of Action)) As MethodInfo
Return CType(expr.Body, MethodCallExpression).Method
End Function
Dim mi As MethodInfo = GetMethod(Sub() Console.WriteLine(""))
y este es el aspecto que podrían tener C#:
public static MethodInfo GetMethod(Expression<Action> expr) =>
(expr.Body as MethodCallExpression).Method;
MethodInfo mi = GetMethod(() => Console.WriteLine(""));
Este enfoque también simplifica la obtención de métodos de extensión y la construcción de métodos genéricos cerrados, como se muestra aquí en Visual Basic:
Dim wherePerson As MethodInfo = GetMethod(Sub() CType(Nothing, IQueryable(Of
Person)).Where(Function(x) True)))
También proporciona una garantía en tiempo de compilación de que existen el método y la sobrecarga, como se muestra aquí en C#:
// Won’t compile, because GetMethod expects Expression<Action>, not Expression<Func<..>>
MethodInfo getMethod = GetMethod(() => GetMethod(() => null));
Configuración de columnas de cuadrícula. Si tiene algún tipo de interfaz de usuario de cuadrícula y desea permitir la definición de las columnas mediante declaración, puede tener un método que cree las columnas basándose en subexpresiones de un literal de matriz (usando Visual Basic):
grid.SetColumns(Function(x As Person) {x.LastName, x.FirstName, x.DateOfBirth})
O subexpresiones de un tipo anónimo (en C#):
grid.SetColumns((Person x) => new {x.LastName, x.FirstName, DOB = x.DateOfBirth});
Uso de los árboles de expresión II: compilación de código invocable en tiempo de ejecución
El segundo caso de uso principal de los árboles de expresión es la generación en tiempo de ejecución de código ejecutable. ¿Recuerda la variable de cuerpo anterior? Puede encapsularla en una expresión lambda, compilarla en un delegado e invocar al delegado, todo ello en tiempo de ejecución. El código sería como el siguiente en Visual Basic:
Dim lambdaExpression = Lambda(Of Action)(body)
Dim compiled = lambdaExpression.Compile
compiled.Invoke
' prints either "Hello" or "Good night"
y como este en C#:
var lambdaExpression = Lambda<Action>(body);
var compiled = lambdaExpression.Compile();
compiled.Invoke();
// Prints either "Hello" or "Good night"
Compilar un árbol de expresión como código ejecutable es muy útil para implementar otros lenguajes basados en CLR, ya que es mucho más fácil trabajar con árboles de expresión que manipular el código IL directamente. Pero si programa en C# o en Visual Basic y conoce la lógica del programa en tiempo de compilación, ¿por qué no insertar esa lógica en el método o el ensamblado actual en tiempo de compilación en lugar de compilarla en tiempo de ejecución?
No obstante, la compilación en tiempo de ejecución destaca realmente cuando no se conoce la mejor ruta de acceso o el algoritmo correcto en tiempo de diseño. Con los árboles de expresión y la compilación en tiempo de ejecución, es relativamente fácil reescribir y ajustar la lógica del programa en tiempo de ejecución en respuesta a las condiciones o a los datos reales del campo.
Código de reescritura automática: almacenamiento en caché del sitio de llamada en lenguajes con tipos dinámicos. Como ejemplo, piense en el almacenamiento en caché del sitio de llamada en Dynamic Language Runtime (DLR), que habilita los tipos dinámicos en las implementaciones de lenguaje dirigidas a CLR. Utiliza árboles de expresión para proporcionar una optimización eficaz reescribiendo de forma iterativa el delegado asignado a un sitio de llamada específico cuando es necesario.
Tanto C# como Visual Basic son, en su mayor parte, lenguajes con tipos estáticos, es decir, para cada expresión del lenguaje, podemos resolver un tipo fijo que no cambia mientras dure el programa. Dicho de otro modo, si las variables x e y se han declarado como Integer (o int en C#) y el programa contiene una línea de código x + y, la resolución del valor de esa expresión siempre usará la instrucción “add” para dos tipos Integer.
Sin embargo, los lenguajes con tipos dinámicos no tienen esta garantía. En general, x e y no tienen ningún tipo inherente, por lo que la evaluación de x + y debe tener en cuenta que x e y pueden ser de cualquier tipo (por ejemplo, String) y la resolución de x + y en ese caso significaría usar String.Concat. Por otro lado, si la primera vez que el programa ejecuta la expresión, x e y son de tipo Integer, es muy probable que los resultados sucesivos tengan los mismos tipos para x e y. DLR aprovecha esto con el almacenamiento en caché del sitio de llamada, que utiliza árboles de expresión para reescribir el delegado del sitio de llamada cada vez que se encuentra un tipo nuevo.
A cada sitio de llamada se le asigna una instancia CallSite(Of T) (o CallSite<T> en C#) con una propiedad de destino que apunta a un delegado compilado. Cada sitio también obtiene un conjunto de pruebas, junto con las acciones que deben realizarse cuando se supera cada prueba. Inicialmente, el delegado Target solo tiene código para actualizarse, por ejemplo:
‘ Visual Basic code representation of Target delegate
Return site.Update(site, x, y)
En la primera iteración, el método Update recupera una prueba aplicable y una acción de la implementación del lenguaje (por ejemplo, “si los dos argumentos son de tipo Integer, usar la instrucción ‘add’”). Después, genera un árbol de expresión que realiza la acción solo si se supera la prueba. El código equivalente al árbol de expresión resultante podría ser parecido a este:
‘ Visual Basic code representation of expression tree
If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y)
Return site.Update(site, x, y)
A continuación, se compila el árbol de expresión en un nuevo delegado y se almacena en la propiedad Target, mientras que la prueba y la acción se almacenan en el objeto CallSite.
En iteraciones posteriores, el sitio de llamada usará el nuevo delegado para resolver x + y. Dentro del nuevo delegado, si se supera la prueba, se usa la operación de CLR resuelta. Solo si las pruebas dan error (en este caso, si x o y no son de tipo Integer), el método Update tiene que recurrir de nuevo a la implementación de lenguaje. Pero cuando se llama al método Update, se agregan la prueba y la acción nuevas y se vuelve a compilar el delegado Target para que las tenga en cuenta. En ese momento, el delegado Target contiene las pruebas para todos los pares de tipos encontrados anteriormente y la estrategia de resolución de valores para cada tipo, como se muestra en el código siguiente:
If TypeOf x Is Integer AndAlso TypeOf y Is Integer Then Return CInt(x) + CInt(y)
If TypeOf x Is String Then Return String.Concat(x, y)
Return site.Update(site, x, y)
Reescribir este código en tiempo de ejecución en respuesta a los hechos que han ocurrido sobre la marcha sería muy difícil, si no imposible, sin la capacidad de compilar código de un árbol de expresión en tiempo de ejecución.
Compilación dinámica con árboles de expresión y con Roslyn. También puede compilar código de forma dinámica a partir de cadenas sencillas, en lugar de hacerlo con un árbol de expresión, de una forma relativamente fácil con Roslyn. De hecho, este enfoque es, en realidad, el recomendado si comienza con sintaxis de Visual Basic o C#, o si es importante preservar la sintaxis de estos lenguajes que haya generado. Como he comentado antes, los árboles de la sintaxis de Roslyn le dan forma a la sintaxis, mientras que los árboles de expresión solo representan operaciones de código sin tener en cuenta la sintaxis.
Además, si intenta construir un árbol de expresión no válido, solo recibirá una excepción. Al analizar y compilar cadenas para el código ejecutable con Roslyn, puede obtener varios fragmentos de información de diagnóstico sobre distintas partes de la compilación, igual que lo haría al escribir código de C# o de Visual Basic en Visual Studio.
Por otro lado, Roslyn es una dependencia grande y compleja para agregarla a un proyecto. Puede que ya disponga de un conjunto de operaciones de código procedentes de código fuente que no era de C# ni de Visual Basic y que no sea necesario reescribirlas en el modelo semántico de Roslyn. Tenga en cuenta también que Roslyn requiere multithreading y no se puede usar si no se permiten nuevos subprocesos (por ejemplo, en un visualizador de depuración de Visual Studio).
Reescritura de árboles de expresión: implementación del operador Like de Visual Basic
He mencionado que los árboles de expresión son inmutables; sin embargo, puede crear un nuevo árbol de expresión que reutilice partes del original. Imaginemos que desea consultar una base de datos para buscar personas cuyo nombre contenga una “e” seguida de una “i”. Visual Basic tiene el operador Like, que devuelve True si una cadena coincide con un patrón, como se muestra aquí:
Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.FirstName Like "*e*i*")
For Each person In qry
Console.WriteLine($"LastName: {person.LastName}, FirstName: {person.FirstName}")
Next
Pero si intenta hacer esto con DbContext en Entity Framework 6, se producirá una excepción con el siguiente mensaje:
“LINQ to Entities no reconoce el método “Boolean LikeString(System.String, System.String, Microsoft.VisualBasic.CompareMethod)” del método, y este método no se puede traducir en una expresión de almacén”.
El operador Like de Visual Basic se resuelve como el método LikeOperator.Like-String (en el espacio de nombres Microsoft.VisualBasic.CompilerServices), que EF6 no puede convertir en una expresión LIKE de SQL. De ahí el error.
Ahora EF6 admite una funcionalidad similar a través del método DbFunctions.Like, que EF6 puede asignar a la expresión LIKE correspondiente. Debe reemplazar el árbol de expresión original, que usa el operador Like de Visual Basic, por uno que use DbFunctions.Like, pero sin cambiar ninguna otra parte del árbol. La expresión común para hacer esto es heredar de la clase ExpressionVisitor de .NET e invalidar los métodos base Visit* que le interesen. En mi caso, como quiero reemplazar una llamada al método, invalidaré VisitMethodCall, como se muestra en la figura 8.
Figura 8. ExpressionTreeVisitor reemplaza el operador Like de Visual Basic por DbFunctions.Like
Class LikeVisitor
Inherits ExpressionVisitor
Shared LikeString As MethodInfo =
GetType(CompilerServices.LikeOperator).GetMethod("LikeString")
Shared DbFunctionsLike As MethodInfo = GetType(DbFunctions).GetMethod(
"Like", {GetType(String), GetType(String)})
Protected Overrides Function VisitMethodCall(
node As MethodCallExpression) As Expression
' Is this node using the LikeString method? If not, leave it alone.
If node.Method <> LikeString Then Return MyBase.VisitMethodCall(node)
Dim patternExpression = node.Arguments(1)
If patternExpression.NodeType = ExpressionType.Constant Then
Dim oldPattern =
CType(CType(patternExpression, ConstantExpression).Value, String)
' partial mapping of Visual Basic's Like syntax to SQL LIKE syntax
Dim newPattern = oldPattern.Replace("*", "%")
patternExpression = Constant(newPattern)
End If
Return [Call](DbFunctionsLike,
node.Arguments(0),
patternExpression
)
End Function
End Class
La sintaxis de patrón del operador Like es diferente de la de la expresión LIKE de SQL, por lo que reemplazaré los caracteres especiales que se usan en el operador Like de Visual Basic por los correspondientes que se utilizan en la expresión LIKE de SQL. Esta asignación está incompleta, no asigna toda la sintaxis de patrón del operador Like de Visual Basic y tampoco agrega un carácter de escape a los caracteres especiales de la expresión LIKE de SQL, o bien quita el carácter de escape de los caracteres especiales del operador Like de Visual Basic. Encontrará una implementación completa en GitHub, en bit.ly/2yku7tx, junto con la versión de C#).
Tenga en cuenta que solo se pueden reemplazar estos caracteres si el patrón forma parte del árbol de expresión y que el nodo de expresión es un objeto Constant. Si el patrón es otro tipo de expresión (como el resultado de una llamada de método o de una expresión binaria que concatene dos cadenas), el valor del patrón no existe hasta que se evalúa la expresión.
Ahora puedo reemplazar la expresión por la que se ha reescrito y usar la nueva expresión en mi consulta, así:
Dim expr As Expression(Of Func(Of Person, Boolean)) =
Function(x) x.FirstName Like "*e*i*"
Dim visitor As New LikeVisitor
expr = CType(visitor.Visit(expr), Expression(Of Func(Of Person, Boolean)))
Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(expr)
For Each person In qry
Console.WriteLine($"LastName: {person.LastName}, FirstName: {person.FirstName}")
Next
Lo ideal sería que este tipo de transformación se hiciera en el proveedor de LINQ to Entities, donde se podría reescribir de una vez el árbol de expresión entero, que podría incluir otros árboles de expresión y llamadas a métodos Queryable, en lugar de tener que reescribir cada expresión antes de pasarla a los métodos Queryable. Pero, en lo esencial, la transformación es la misma: alguna clase o función que visita todos los nodos y conecta un nodo de reemplazo cuando es necesario.
Resumen
Los árboles de expresión le dan forma a varias operaciones de código y se pueden usar para exponer API sin necesidad de que los desarrolladores aprendan un nuevo lenguaje o vocabulario. Un desarrollador puede aprovechar Visual Basic o C# para que se puedan usar estas API, mientras que el compilador proporciona la comprobación de tipos y la corrección de sintaxis y el IDE proporciona IntelliSense. Se puede crear una copia modificada de un árbol de expresión en la que se hayan agregado, quitado o reemplazado nodos. Los árboles de expresión también se pueden usar para compilar código de forma dinámica en tiempo de ejecución y para la reescritura automática de código, incluso cuando Roslyn es la ruta de acceso preferida para la compilación dinámica.
Los ejemplos de código de este artículo se encuentran en bit.ly/2yku7tx.
Más información
- Árboles de expresión en las guías de programación de Visual Basic (bit.ly/2Msocef) y C# (bit.ly/2Y9q5nj).
- Expresiones lambda en las guías de programación de Visual Basic (bit.ly/2YsZFs3) y C# (bit.ly/331ZWp5).
- Proyecto de mejoras de los árboles de expresión de Bart De Smet (bit.ly/2OrUsRw).
- El proyecto de DLR en GitHub (bit.ly/2yssz0x) tiene documentos que explican el diseño de los árboles de expresión en .NET.
- Representación de los árboles de expresión como cadenas y depuración de visualizadores para árboles de expresión: bit.ly/2MsoXnB y bit.ly/2GAp5ha.
- “What does Expression.Quote() do that Expression.Constant() can’t already do?” (¿Qué hace Expression.Quote() que no haga ya Expression.Constant()?) en StackOverflow (bit.ly/30YT6Pi)
Zev Spitzha escrito una biblioteca para representar los árboles de expresión como cadenas con varios formatos: C#, Visual Basic y llamadas a métodos del patrón de diseño Factory Method; así como un visualizador de depuración de Visual Studio para árboles de expresión.
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Kathleen Dollard