¿Qué hace Expression.Quote () que Expression.Constant () ya no puede hacer?


97

Nota: Soy consciente de la pregunta anterior “ ¿Cuál es el propósito del método Expression.Quote de LINQ? , Pero si sigue leyendo, verá que no responde a mi pregunta.

Entiendo cuál es el propósito declarado de Expression.Quote(). Sin embargo, Expression.Constant()se puede usar para el mismo propósito (además de todos los propósitos para los que Expression.Constant()ya se usa). Por lo tanto, no entiendo por qué Expression.Quote()se requiere en absoluto.

Para demostrar esto, he escrito un ejemplo rápido donde uno usaría habitualmente Quote(vea la línea marcada con signos de exclamación), pero usé en su Constantlugar y funcionó igualmente bien:

string[] array = { "one", "two", "three" };

// This example constructs an expression tree equivalent to the lambda:
// str => str.AsQueryable().Any(ch => ch == 'e')

Expression<Func<char, bool>> innerLambda = ch => ch == 'e';

var str = Expression.Parameter(typeof(string), "str");
var expr =
    Expression.Lambda<Func<string, bool>>(
        Expression.Call(typeof(Queryable), "Any", new Type[] { typeof(char) },
            Expression.Call(typeof(Queryable), "AsQueryable",
                            new Type[] { typeof(char) }, str),
            // !!!
            Expression.Constant(innerLambda)    // <--- !!!
        ),
        str
    );

// Works like a charm (prints one and three)
foreach (var str in array.AsQueryable().Where(expr))
    Console.WriteLine(str);

La salida de expr.ToString()es la misma para ambos también (ya sea que use Constanto Quote).

Dadas las observaciones anteriores, parece que Expression.Quote()es redundante. El compilador de C # podría haberse hecho para compilar expresiones lambda anidadas en un árbol de expresión que involucre en Expression.Constant()lugar de Expression.Quote(), y cualquier proveedor de consultas LINQ que desee procesar árboles de expresión en algún otro lenguaje de consulta (como SQL) podría buscar un ConstantExpressiontipo with en Expression<TDelegate>lugar de a UnaryExpressioncon el Quotetipo de nodo especial , y todo lo demás sería igual.

¿Qué me estoy perdiendo? ¿Por qué se inventó Expression.Quote()y el Quotetipo de nodo especial para UnaryExpression?

Respuestas:


189

Respuesta corta:

El operador de comillas es un operador que induce semántica de cierre en su operando . Las constantes son solo valores.

Las comillas y las constantes tienen diferentes significados y, por lo tanto, tienen diferentes representaciones en un árbol de expresión . Tener la misma representación para dos cosas muy diferentes es extremadamente confuso y propenso a errores.

Respuesta larga:

Considera lo siguiente:

(int s)=>(int t)=>s+t

La lambda externa es una fábrica de sumadores que están vinculados al parámetro de la lambda externa.

Ahora, supongamos que deseamos representar esto como un árbol de expresión que luego se compilará y ejecutará. ¿Cuál debería ser el cuerpo del árbol de expresión? Depende de si desea que el estado compilado devuelva un delegado o un árbol de expresión.

Comencemos por descartar el caso poco interesante. Si deseamos que devuelva un delegado, entonces la cuestión de si usar Quote o Constant es un punto discutible:

        var ps = Expression.Parameter(typeof(int), "s");
        var pt = Expression.Parameter(typeof(int), "t");
        var ex1 = Expression.Lambda(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt),
            ps);

        var f1a = (Func<int, Func<int, int>>) ex1.Compile();
        var f1b = f1a(100);
        Console.WriteLine(f1b(123));

La lambda tiene una lambda anidada; el compilador genera la lambda interior como un delegado de una función cerrada sobre el estado de la función generada para la lambda exterior. No necesitamos considerar este caso más.

Supongamos que deseamos que el estado compilado devuelva un árbol de expresión del interior. Hay dos formas de hacerlo: la forma fácil y la forma difícil.

La manera difícil es decir que en lugar de

(int s)=>(int t)=>s+t

lo que realmente queremos decir es

(int s)=>Expression.Lambda(Expression.Add(...

Y luego genere el árbol de expresión para eso , produciendo este lío :

        Expression.Lambda(
            Expression.Call(typeof(Expression).GetMethod("Lambda", ...

bla, bla, bla, docenas de líneas de código de reflexión para hacer la lambda. El propósito del operador de comillas es decirle al compilador del árbol de expresiones que queremos que la lambda dada sea tratada como un árbol de expresión, no como una función, sin tener que generar explícitamente el código de generación del árbol de expresión .

La forma fácil es:

        var ex2 = Expression.Lambda(
            Expression.Quote(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f2a = (Func<int, Expression<Func<int, int>>>)ex2.Compile();
        var f2b = f2a(200).Compile();
        Console.WriteLine(f2b(123));

Y de hecho, si compila y ejecuta este código, obtiene la respuesta correcta.

Observe que el operador de comillas es el operador que induce la semántica de cierre en la lambda interior que usa una variable externa, un parámetro formal de la lambda externa.

La pregunta es: ¿por qué no eliminar Quote y hacer que esto haga lo mismo?

        var ex3 = Expression.Lambda(
            Expression.Constant(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f3a = (Func<int, Expression<Func<int, int>>>)ex3.Compile();
        var f3b = f3a(300).Compile();
        Console.WriteLine(f3b(123));

La constante no induce semántica de cierre. ¿Por qué debería hacerlo? Dijiste que esto era una constante . Es solo un valor. Debería ser perfecto cuando se entregó al compilador; el compilador debería poder generar un volcado de ese valor en la pila donde sea necesario.

Dado que no se induce un cierre, si hace esto, obtendrá una excepción "variable 's' de tipo 'System.Int32' no está definida" en la invocación.

(Aparte: acabo de revisar el generador de código para la creación de delegados a partir de árboles de expresión entre comillas, y desafortunadamente un comentario que puse en el código en 2006 todavía está allí. Para su información, el parámetro externo elevado se captura en una constante cuando se cita El árbol de expresión se reifica como un delegado por el compilador en tiempo de ejecución.Había una buena razón por la que escribí el código de esa manera que no recuerdo en este momento exacto, pero tiene el desagradable efecto secundario de introducir cierre sobre valores de parámetros externos en lugar de cierre sobre variables. Aparentemente, el equipo que heredó ese código decidió no corregir ese defecto, por lo que si confía en la mutación de un parámetro externo cerrado que se observa en una lambda interior citada compilada, se sentirá decepcionado. Sin embargo, dado que es una práctica de programación bastante mala tanto (1) mutar un parámetro formal como (2) depender de la mutación de una variable externa, le recomendaría que cambie su programa para no usar estas dos prácticas de programación malas, en lugar de esperando una solución que no parece estar disponible. Disculpas por el error.)

Entonces, para repetir la pregunta:

El compilador de C # podría haberse hecho para compilar expresiones lambda anidadas en un árbol de expresión que involucre Expression.Constant () en lugar de Expression.Quote (), y cualquier proveedor de consultas LINQ que desee procesar árboles de expresión en algún otro lenguaje de consulta (como SQL ) podría buscar una ConstantExpression con el tipo Expression en lugar de una UnaryExpression con el tipo de nodo Quote especial, y todo lo demás sería igual.

Estás en lo correcto. Nos podríamos codificar la información semántica que significa "inducen la semántica de cierre de este valor" por el uso del tipo de la expresión de la constante como una bandera .

"Constante" tendría entonces el significado "use este valor constante, a menos que el tipo sea un tipo de árbol de expresión y el valor sea un árbol de expresión válido, en cuyo caso, use en su lugar el valor que es el árbol de expresión resultante de reescribir el interior del árbol de expresión dado para inducir la semántica de cierre en el contexto de cualquier lambda exterior en el que podríamos estar ahora.

Pero ¿por qué tendríamos que hacer esa cosa loca? El operador de cotización es un operador increíblemente complicado y debe usarse explícitamente si lo va a usar. Está sugiriendo que para ser parsimoniosos al no agregar un método de fábrica adicional y un tipo de nodo entre las varias docenas que ya existen, agreguemos un extraño caso de esquina a las constantes, de modo que las constantes a veces sean lógicamente constantes, y a veces se reescriban lambdas con semántica de cierre.

También tendría el efecto algo extraño de que constante no significa "usar este valor". Suponga que, por alguna extraña razón, desea que el tercer caso anterior compile un árbol de expresión en un delegado que distribuya un árbol de expresión que tenga una referencia no reescrita a una variable externa. ¿Por qué? Quizás porque está probando su compilador y simplemente desea pasar la constante para poder realizar algún otro análisis más adelante. Su propuesta lo haría imposible; cualquier constante que sea del tipo árbol de expresión se reescribirá independientemente. Uno tiene una expectativa razonable de que "constante" significa "usar este valor". "Constante" es un nodo "haz lo que digo". El procesador constante ' decir basado en el tipo.

Y tenga en cuenta, por supuesto, que ahora está poniendo la carga de la comprensión (es decir, la comprensión de que constante tiene una semántica complicada que significa "constante" en un caso e "induce una semántica de cierre" basada en una bandera que está en el sistema de tipos ) sobre cada proveedor que realiza análisis semántico de un árbol de expresión, no solo de proveedores de Microsoft. ¿Cuántos de esos proveedores externos se equivocarían?

"Quote" agita una gran bandera roja que dice "Hey amigo, mira aquí, soy una expresión lambda anidada y tengo una semántica loca si estoy cerrado sobre una variable externa". mientras que "Constante" está diciendo "No soy más que un valor; úsame como mejor te parezca". Cuando algo es complicado y peligroso, queremos hacer que ondee banderas rojas, sin ocultar ese hecho haciendo que el usuario busque en el sistema de tipos para averiguar si este valor es especial o no.

Además, la idea de que evitar la redundancia es incluso un objetivo es incorrecta. Claro, evitar la redundancia innecesaria y confusa es un objetivo, pero la mayoría de la redundancia es algo bueno; la redundancia crea claridad. Los nuevos métodos de fábrica y tipos de nodos son baratos . Podemos hacer tantas como necesitemos para que cada una represente una operación limpiamente. No tenemos necesidad de recurrir a trucos desagradables como "esto significa una cosa a menos que este campo esté configurado para esta cosa, en cuyo caso significa otra cosa".


11
Estoy avergonzado ahora porque no pensé en la semántica de cierre y no pude probar un caso en el que la lambda anidada captura un parámetro de una lambda externa. Si hubiera hecho eso, habría notado la diferencia. Muchas gracias nuevamente por tu respuesta.
Timwi

19

Esta pregunta ya ha recibido una excelente respuesta. Además, me gustaría señalar un recurso que puede resultar útil con preguntas sobre árboles de expresión:

Ahí es fue un proyecto CodePlex de Microsoft llamado Tiempo de ejecución de lenguaje dinámico. Su documentación incluye el documento titulado,"Especificación de árboles de expresión v2", que es exactamente eso: la especificación para árboles de expresión LINQ en .NET 4.

Actualización: CodePlex está inactivo. La especificación Expression Trees v2 (PDF) se ha trasladado a GitHub .

Por ejemplo, dice lo siguiente sobre Expression.Quote:

4.4.42 Cotización

Use Quote en UnaryExpressions para representar una expresión que tiene un valor "constante" de tipo Expresión. A diferencia de un nodo Constante, el nodo Quote maneja especialmente los nodos ParameterExpression contenidos. Si un nodo ParameterExpression contenido declara un local que se cerraría en la expresión resultante, Quote reemplaza ParameterExpression en sus ubicaciones de referencia. En tiempo de ejecución, cuando se evalúa el nodo Quote, sustituye las referencias de la variable de cierre por los nodos de referencia ParameterExpression y luego devuelve la expresión entre comillas. […] (Págs. 63–64)


1
Excelente respuesta del tipo enseñar a un hombre a pescar. Solo me gustaría agregar que la documentación se ha movido y ahora está disponible en docs.microsoft.com/en-us/dotnet/framework/… . El documento citado, específicamente, está en GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relativamente_random

3

Después de esto, una respuesta realmente excelente, está claro qué es la semántica. No está tan claro por qué están diseñados de esa manera, considere:

Expression.Lambda(Expression.Add(ps, pt));

Cuando se compila e invoca esta lambda, evalúa la expresión interna y devuelve el resultado. La expresión interna aquí es una adición, por lo que ps + pt se evalúa y se devuelve el resultado. Siguiendo esta lógica, la siguiente expresión:

Expression.Lambda(
    Expression.Lambda(
              Expression.Add(ps, pt),
            pt), ps);

debe devolver una referencia de método compilado lambda interno cuando se invoca la lambda externa (porque decimos que lambda se compila en una referencia de método). Entonces, ¿por qué necesitamos una cotización? Para diferenciar el caso cuando se devuelve la referencia del método frente al resultado de esa invocación de referencia.

Específicamente:

let f = Func<...>
return f; vs. return f(...);

Por alguna razón, los diseñadores de .Net eligieron Expression.Quote (f) para el primer caso y f simple para el segundo. En mi opinión, esto causa una gran confusión, ya que en la mayoría de los lenguajes de programación devolver un valor es directo (no es necesario Quote o cualquier otra operación), pero la invocación requiere escritura adicional (paréntesis + argumentos), lo que se traduce en algún tipo de invocar a nivel MSIL. Los diseñadores de .Net hicieron lo contrario para los árboles de expresión. Sería interesante conocer el motivo.


-2

Creo que el punto aquí es la expresividad del árbol. La expresión constante que contiene al delegado en realidad solo contiene un objeto que resulta ser un delegado. Esto es menos expresivo que un desglose directo de una expresión unaria y binaria.


¿Lo es? ¿Qué expresividad agrega exactamente? ¿Qué puedes "expresar" con esa UnaryExpression (que también es un tipo de expresión extraño de usar) que no pudiste expresar con ConstantExpression?
Timwi
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.