Dado el siguiente código:

using System;

class MyClass
{
    public MyClass x;
}

public static class Program
{
    public static void Main()
    {
        var a = new MyClass();
        var b = new MyClass();
        a.x = (a = b);
        Console.WriteLine(a.x == a);
    }
}

Las dos primeras líneas son muy obvias, solo dos objetos diferentes.

Asumo que la tercera línea hace lo siguiente:

  • La parte (a = b) asigna b a a y devuelve b, por lo que ahora a es igual a b.
  • Luego, a.x se asigna a b.

Eso significa que a.x es igual a b, y también b es igual a a. Lo que implica que a.x es igual a a.

Sin embargo, el código imprime False.

¿Qué pasa?

c#
27
Youssef13 29 may. 2020 a las 22:16

4 respuestas

La mejor respuesta

Ocurre porque está intentando actualizar a dos veces en la misma declaración. a en a.x= se refiere a la instancia anterior. Por lo tanto, está actualizando a para hacer referencia a b y el antiguo campo de objeto a x para hacer referencia a b.

Puedes confirmar con esto:

void Main()
{
    var a = new MyClass(){s="a"};
    var b = new MyClass() {s="b"};
    var c =a;

    a.x = (a=b);
    Console.WriteLine($"a is {a.s}");
    Console.WriteLine(a.x == b);

    Console.WriteLine($"c is {c.s}");       
    Console.WriteLine(c.x == b);
}

class MyClass
{
    public MyClass x;
    public string s;
}

La respuesta será:

a is b
False
c is a
True

Editar: solo para aclarar un poco más, no se trata del orden de ejecución, se debe a las dos actualizaciones en la misma variable en la misma declaración. No importa si (a=b) se ejecuta primero o no, porque a.x hace referencia a la instancia anterior, no a la recién actualizada.

8
user2357112 supports Monica 30 may. 2020 a las 04:08

Interesante hallazgo, puse tu código en Sharplab y verifiqué qué sucede.

Parece que los intercambios del compilador dejaron operandos en su asignación, esto es lo que parece descompilado de nuevo a C # (los nombres de las variables se cambian):

public static void Main()
{
    MyClass myClass = new MyClass();
    MyClass x = new MyClass();
    myClass = (myClass.x = x);
    Console.WriteLine(myClass.x == myClass);
}

Entonces, lo que sucede es que a.x se convierte en b y luego b se asigna a a. Tanto la variable local a como el atributo a.x ahora apuntan al objeto b. Entonces:

  • a puntos variables al objeto b
  • b puntos variables al objeto b
  • El atributo a objeto x apunta al objeto b
  • El atributo b objeto x es nulo

Cambié un poco tu código para ilustrarlo mejor:

public static void Main(string[] args)
{
    var a = new MyClass();
    var originalA = a;
    a.Name = "a";
    var b = new MyClass();
    b.Name = "b";
    a.x = (a = b);

    Console.WriteLine(a.x == a);

    Console.WriteLine("a           - " + a.Name);
    Console.WriteLine("a.x         - " + a.x?.Name);

    Console.WriteLine("b           - " + b.Name);
    Console.WriteLine("b.x         - " + b.x?.Name);

    Console.WriteLine("originalA   - " + originalA.Name);
    Console.WriteLine("originalA.x - " + originalA.x?.Name);
}

Ese código devuelve:

False
a           - b
a.x         - 
b           - b
b.x         - 
originalA   - a
originalA.x - b

Observe que solo originalA ahora apunta al objeto real a, otras variables locales ahora apuntan a b.

No es un error del compilador; vea la respuesta de Magnetron.

2
Daniel Bider 29 may. 2020 a las 20:12

En a.x = (a = b), el lado izquierdo a.x se evalúa primero para encontrar el objetivo de la asignación, luego se evalúa el lado derecho.

Esto también me sorprendió, porque intuitivamente creo que comienza en el lado derecho y evalúa hacia la izquierda, pero este no es el caso. (El asociatividad es de derecha a izquierda, lo que significa que los paréntesis en este caso no son necesarios).

Aquí está la especificación llamando al pedido las cosas suceden en, con los bits relevantes citados a continuación:

El procesamiento en tiempo de ejecución de una asignación simple del formulario x = y consta de los siguientes pasos:

  • Si x se clasifica como una variable:
    • x se evalúa para producir la variable.
    • y se evalúa y, si es necesario, se convierte al tipo de x a través de una conversión implícita.
    • [...]
    • El valor resultante de la evaluación y conversión de y se almacena en la ubicación dada por la evaluación de x.

En cuanto a la IL generada por el enlace sharplab Pavel colocado:

        // stack is empty []
newobj instance void MyClass::.ctor()
        // new instance of MyClass on the heap, call it $0
        // stack -> [ref($0)]
stloc.0
        // stack -> []
        // local[0] ("a") = ref($0)
newobj instance void MyClass::.ctor()
        // new instance of MyClass on the heap, call it $1
        // stack -> [ref($1)]
stloc.1
        // stack -> []
        // local[1] ("b") = ref($1)
ldloc.0
        // stack -> [ref($0)]
ldloc.1
        // stack -> [ref($1), ref($0)]
dup
        // stack -> [ref($1), ref($1), ref($0)]
stloc.0
        // stack -> [ref($1), ref($0)]
        // local[0] ("a") = $1
stfld class MyClass MyClass::x
        // stack -> []
        // $0.x = $1
7
Joe Sewell 29 may. 2020 a las 20:22

Solo para agregar un poco de IL diversión a la discusión:

El encabezado del método Main se ve a continuación:

method private hidebysig static void
    Main() cil managed
  {
    .maxstack 3
    .locals init (
      [0] class MyClass a,
      [1] class MyClass b
    )

La declaración a.x = (a=b); se traduce a la siguiente IL:

IL_000d: ldloc.0      // a
IL_000e: ldloc.1      // b
IL_000f: dup
IL_0010: stloc.0      // a
IL_0011: stfld        class MyClass::x

Las dos primeras instrucciones de carga (ldloc.0, ldloc.1) en las referencias de la pila de evaluación almacenadas en las variables a y b, llamémoslas aRef y bRef, así que tenemos siguiente estado de la pila de evaluación:

bRef
aRef

El {{X0 }} la instrucción copia el valor más alto actual en la pila de evaluación y luego empuja la copia a la pila de evaluación:

bRef
bRef
aRef

El stloc.0 muestra el valor actual desde la parte superior de la pila de evaluación y lo almacena en la lista de variables locales en el índice 0 (a la variable se establece en bRef), dejando la pila en el siguiente estado:

bRef
aRef

Y finalmente {{ X0}} saca de la pila el valor (bRef) y la referencia / puntero del objeto (aRef). El valor del campo en el objeto (aRef.x) se reemplaza con el valor proporcionado (bRef).

Todo lo cual da como resultado el comportamiento descrito en la publicación, con ambas variables (a y b) apuntando a bRef con bRef.x siendo nulo y aRef.x apuntando a bRef, que puede verificarse con una variable adicional que contiene aRef como se sugiere @Magnetron.

4
Guru Stron 29 may. 2020 a las 20:31