2010-02-16 5 views
5

Al llamar Push() y Pop() una instancia de Stack<T> en una sola línea consigo un comportamiento diferente de realizar el mismo código en mi humilde opinión en dos líneas.orden de operación inesperado en Pila <T> relacionados con un trazador de líneas

El siguiente fragmento de código reproduce el comportamiento:

static void Main(string[] args) 
{ 
Stack<Element> stack = new Stack<Element>(); 
Element e1 = new Element { Value = "one" }; 
Element e2 = new Element { Value = "two" }; 
stack.Push(e1); 
stack.Push(e2); 

Expected(stack); // element on satck has value "two" 
//Unexpected(stack); // element on stack has value "one" 

Console.WriteLine(stack.Peek().Value); 
Console.ReadLine(); 
} 

public static void Unexpected(Stack<Element> stack) 
{ 
stack.Peek().Value = stack.Pop().Value; 
} 

public static void Expected(Stack<Element> stack) 
{ 
Element e = stack.Pop(); 
stack.Peek().Value = e.Value; 
} 

La clase de elemento es muy básico:

public class Element 
{ 
public string Value 
{ 
    get; 
    set; 
} 
} 

Con este código me sale el siguiente resultado (.NET 3.5, Win 7, totalmente parcheado):

  • Calling Expected() (versión con dos líneas ) LEA un elemento en la pila con Value establecido en "two".
  • Al llamar Unexpected() (Versión con una línea) consigo un elemento de la pila con el conjunto de Value "one".

La única razón de la diferencia que podía imaginar era la precedencia del operador. Como el operador de asignación (=) tiene el lowest precedence, no veo ninguna razón por la cual los dos métodos se comporten de manera diferente.

También tuve un vistazo a la IL genera:

.method public hidebysig static void Unexpected(class [System]System.Collections.Generic.Stack`1<class OperationOrder.Element> stack) cil managed 
{ 
    .maxstack 8 
    L_0000: ldarg.0 
    L_0001: callvirt instance !0 [System]System.Collections.Generic.Stack`1<class OperationOrder.Element>::Peek() 
    L_0006: ldarg.0 
    L_0007: callvirt instance !0 [System]System.Collections.Generic.Stack`1<class OperationOrder.Element>::Pop() 
    L_000c: callvirt instance string OperationOrder.Element::get_Value() 
    L_0011: callvirt instance void OperationOrder.Element::set_Value(string) 
    L_0016: ret 
} 

No soy una grieta IL, pero para mí este código todavía se ve bien ans deben salir de un elemento en la pila con el valor establecido en " dos".

¿Alguien me puede explicar el motivo por el cual el método Unexpected() hace algo diferente de Expected()?

¡Muchas gracias!

Lukas

Respuesta

9

En C# los operandos se evalúan de izquierda a derecha. Siempre siempre siempre de izquierda a derecha. Por lo tanto, los operandos del operador = se evalúan de izquierda a derecha. En su ejemplo "esperado", la expresión Pop() ocurre en una declaración que se ejecuta antes de la declaración de la expresión Peek(). En su ejemplo "inesperado", la expresión Peek() está a la izquierda de la expresión Pop(), por lo que se evalúa primero.

La respuesta de respuesta de SLaks es que el receptor de una llamada siempre se evalúa antes que los argumentos de la llamada. Esto es correcto, ¡porque el receptor de una llamada siempre está a la izquierda de los argumentos! Pero el aserto de SLaks de que esto tiene algo que ver con el hecho de que es un setter de propiedad es incorrecto.Obtendrá exactamente el mismo comportamiento si Value fuera un campo; la expresión que contiene el acceso de campo está a la izquierda del valor asignado, y por lo tanto se calcula primero.

Mencionaste "precedencia", lo que indica que probablemente te suscribas a la noción completamente mítica y totalmente falsa de que la precedencia tiene algo que ver con el orden de ejecución. No es. Descansa de tu creencia en este mito. El orden de ejecución de las subexpresiones es de izquierda a derecha. La operación de los operadores se realiza en orden de precedencia.

Por ejemplo, considere F() + G() * H(). * tiene una precedencia mayor que +. En su mundo mítico, la operación de precedencia más alta se realiza primero, entonces G() se evalúa, luego H(), luego se multiplican, luego F(), luego la suma.

Esto es total y absolutamente incorrecto. Dilo conmigo: la precedencia no tiene nada que ver con el orden de ejecución. Las subexpresiones se evalúan de izquierda a derecha, de modo que primero evaluamos F(), luego G(), luego H(). Luego calculamos el producto del resultado de G() y H(). Luego calculamos la suma del producto con el resultado de F(). Es decir, esta expresión es equivalente a:

temp1 = F(); 
temp2 = G(); 
temp3 = H(); 
temp4 = temp2 * temp3; 
result = temp1 + temp4; 

El operador = es un operador como cualquier otro; un operador de baja precedencia, pero un operador. Sus operandos se evalúan de izquierda a derecha, y como es un operador de baja precedencia, el efecto del operador (la asignación) se realiza más tarde que los efectos de todos los demás operadores. El efecto del operador y el cálculo de sus operandos son cosas completamente diferentes. El primero se hace en orden de precedencia. Este último se realiza en orden de izquierda a derecha.

¿Está claro?

ACTUALIZACIÓN: Confundir precedencia, asociatividad y orden de ejecución es muy común; muchos autores de libros con una larga experiencia en el diseño de lenguaje de programación se equivocan. C# tiene reglas muy estrictas que definen cada una; si usted está interesado en obtener más información acerca de cómo funciona todo esto, que podrían estar interesados ​​en estos artículos que he escrito sobre el tema:

http://blogs.msdn.com/ericlippert/archive/tags/precedence/default.aspx

+0

¿No hay '=' azúcar sintáctico para el establecimiento de la propiedad aquí, y no es un operador? – SLaks

+0

Me doy cuenta de que el comportamiento sería el mismo para una asignación normal (de campo o local), pero quería vincular a la parte correcta de la especificación. – SLaks

+0

Perfectamente claro ahora. Y lo recordaré, ¡lo prometo! –

Cuestiones relacionadas