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

c# expression-trees

Pregunta

Nota: soy consciente de la pregunta anterior: ¿Cuál es el propósito del método de Expresión.Cita de LINQ? â € ” Pero si sigues leyendo, verás 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 ya se usa Expression.Constant() ). Por lo tanto, no entiendo por qué se requiere Expression.Quote() .

Para demostrar esto, he escrito un ejemplo rápido en el que uno usaría habitualmente Quote (vea la línea marcada con signos de exclamación), pero en su lugar usé Constant 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 Constant o 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 Expression.Constant() incluya Expression.Constant() lugar de Expression.Quote() , y cualquier proveedor de consultas LINQ que desee procesar árboles de expresiones en algún otro lenguaje de consulta (como SQL ) podría buscar una Expression<TDelegate> ConstantExpression con el tipo de Expression<TDelegate> lugar de una UnaryExpression con el tipo de nodo de Quote especial, y todo lo demás sería igual.

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

Respuesta aceptada

Respuesta corta:

El operador de cotización 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 expresiones . 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 para sumadores que están vinculados al parámetro de lambda externa.

Ahora, supongamos que deseamos representar esto como un árbol de expresión que luego será compilado y ejecutado. ¿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 expresiones.

Vamos a empezar por desestimar el caso poco interesante. Si deseamos que devuelva un delegado, entonces la cuestión de si usar la Cotización o la Constante 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 el lambda interior como un delegado a una función cerrada sobre el estado de la función generada para el lambda externo. Necesitamos considerar este caso no más.

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

La manera más 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 generar 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 cotización es decirle al compilador del árbol de expresiones que queremos que el lambda dado se trate como un árbol de expresiones, no como una función, sin tener que generar explícitamente el código de generación del árbol de expresiones .

La forma más 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 compilas y ejecutas este código obtienes la respuesta correcta.

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

La pregunta es: ¿por qué no eliminar Cita 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 el cierre semántico. ¿Por qué debería? Dijiste que esto era una constante . Es sólo un valor. Debe ser perfecto como se entregó al compilador; el compilador debería poder generar un volcado de ese valor a la pila donde se necesita.

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

(Aparte: acabo de revisar el generador de código para la creación de delegados a partir de árboles de expresiones citados, 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 aparece el El compilador de runtime ha reificado el árbol de expresiones como delegado. Hubo una buena razón por la que escribí el código de esa manera que no recuerdo en este momento exacto, pero tiene el efecto secundario desagradable de introducir el cierre sobre los valores de los parámetros externos En lugar del cierre sobre las 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 un lambda interior citado y compilado, 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) basarse en la mutación de una variable externa, le recomendaría que cambie su programa para no usar estos dos malos. las prácticas de programación, en lugar de esperar 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 expresiones que incluya Expression.Constant () en lugar de Expression.Quote (), y cualquier proveedor de consultas LINQ que desee procesar árboles de expresiones en algún otro lenguaje de consulta (como SQL ) podría buscar una expresión constante con el tipo de expresión en lugar de una expresión unaria con el tipo de nodo de cotización especial, y todo lo demás sería igual.

Estás en lo correcto. Podríamos codificar la información semántica que significa "inducir la semántica de cierre de este valor" mediante 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 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 podamos estar en este momento.

Pero ¿por qué tenemos que hacer algo loco? El operador de cotización es un operador increíblemente complicado , y debe usarse explícitamente si lo va a utilizar. Usted está sugiriendo que para ser parsimonioso acerca de no agregar un método de fábrica y un tipo de nodo adicionales entre las varias docenas que ya existen, agregamos un caso de esquina extraño a las constantes, de modo que las constantes a veces son lógicamente constantes, y otras veces se reescriben Lambdas con cierre semántico.

También tendría el efecto un tanto extraño de que constante no signifique "usar este valor". Supongamos que, por alguna extraña razón, quisiera que el tercer caso anterior compilara un árbol de expresiones en un delegado que distribuya un árbol de expresiones que tenga una referencia no reescrita a una variable externa. ¿Por qué? Tal vez porque está probando su compilador y desea simplemente pasar la constante para poder realizar algún otro análisis más adelante. Tu propuesta lo haría imposible; Cualquier constante que resulte ser del tipo de árbol de expresiones sería reescrita independientemente. Uno tiene una expectativa razonable de que "constante" significa "usar este valor". "Constante" es un nodo "haz lo que digo". El trabajo del procesador constante no es adivinar lo que querías decir según el tipo.

Y, por supuesto, tenga en cuenta que ahora está poniendo la carga de la comprensión (es decir, entender que la constante tiene una semántica complicada que significa "constante" en un caso y "inducir una semántica de cierre" basada en una bandera que está en el sistema de tipos ) en cada Proveedor que realiza análisis semánticos de un árbol de expresiones, no solo de los proveedores de Microsoft. ¿Cuántos de esos proveedores terceros se equivocan?

"Cita" es agitar una gran bandera roja que dice "hey amigo, mira aquí, soy una expresión lambda anidada y tengo una semántica extravagante si estoy cerrado sobre una variable externa". mientras que "Constante" está diciendo "No soy más que un valor; utilíceme como mejor le parezca". Cuando algo es complicado y peligroso, queremos que lo hagan ondear banderas rojas, no ocultando ese hecho haciendo que el usuario revise 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 una 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 sea necesario 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".


Respuesta popular

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

Hay un proyecto CodePlex de Microsoft llamado Dynamic Language Runtime . Su documentación incluye el documento titulado "Expression Trees v2 Spec" , que es exactamente eso: La especificación para los árboles de expresión LINQ en .NET 4.

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 Constant, 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, entonces Quote reemplaza la ParameterExpression en sus ubicaciones de referencia. En el tiempo de ejecución, cuando se evalúa el nodo de Cotización, sustituye las referencias de la variable de cierre por los nodos de referencia de ParameterExpression y luego devuelve la expresión citada. […] (P. 63–64)



Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow