Неожиданное неравенство после присваивания - PullRequest
27 голосов
/ 29 мая 2020

Учитывая следующий код:

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);
    }
}

Первые две строки очень очевидны, всего лишь два разных объекта.

Я предполагаю, что третья строка делает следующее:

  • Часть (a = b) присваивает b a и возвращает b, поэтому теперь a равно b.
  • Затем a.x присваивается b.

Это означает, что a.x равно b, а также b равно a. Это означает, что a.x равно a.

Однако код печатает False.

Что происходит?

Ответы [ 4 ]

21 голосов
/ 29 мая 2020

Это происходит из-за того, что вы дважды пытаетесь обновить a в одном и том же операторе. a в a.x= относится к старому экземпляру. Итак, вы обновляете a для ссылки b и старое a поле объекта x для ссылки b.

Вы можете подтвердить это:

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;
}

Ответ будет:

a is b
False
c is a
True

Изменить: для большей ясности. Дело не в порядке выполнения операторов, а из-за двух обновлений одной и той же переменной в одном операторе. . Присвоение (a=b) выполняется перед a.x=, но это не имеет значения, потому что a.x ссылается на старый экземпляр, а не на новый обновленный. Это происходит, как объясняет ответ @Joe Sewell, потому что оценка для поиска цели назначения выполняется слева направо.

17 голосов
/ 29 мая 2020

В a.x = (a = b) левая часть a.x сначала оценивается, чтобы найти цель присваивания, затем оценивается правая часть.

Это тоже было удивительно для меня, потому что я интуитивно думаю начинается с правой стороны и оценивается слева, но это не так. ( ассоциативность идет справа налево, что означает, что скобки в этом случае не нужны.)

Вот спецификация, вызывающая порядок вещей происходит в с соответствующими битами, указанными ниже:

Обработка во время выполнения простого присваивания формы x = y состоит из следующих шагов:

  • Если x классифицируется как переменная:
    • x вычисляется для создания переменной.
    • y оценивается и, при необходимости, преобразуется в тип x посредством неявного преобразования.
    • [...]
    • Значение, полученное в результате оценки и преобразования y, сохраняется в месте, заданном оценкой x.

Глядя на IL, сгенерированный ссылкой на sharplab Павел написал:

        // 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") = ref($1)
stfld class MyClass MyClass::x
        // stack -> []
        // $0.x = ref($1)
8 голосов
/ 29 мая 2020

Просто, чтобы добавить IL веселья в обсуждение:

Заголовок метода Main выглядит следующим образом:

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

Оператор a.x = (a=b); переводится в следующий 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

Первые две инструкции загружаются ( ldlo c .0 , ldlo c .1 ) в ссылки стека оценки, хранящиеся в Переменные a и b, давайте назовем их aRef и bRef, чтобы получить следующее состояние стека оценки:

bRef
aRef

Инструкция dup копирует текущее самое верхнее значение в стеке оценки, а затем помещает копию в стек оценки:

bRef
bRef
aRef

stlo c .0 выталкивает текущее значение из вершины оценки стек и сохраняет его в списке локальных переменных с индексом 0 (a переменная установлена ​​на bRef), оставляя стек в следующем состоянии:

bRef
aRef

И, наконец, stfld извлекает из стека значение (bRef) и ссылку на объект / указатель (aRef). Значение поля в объекте (aRef.x) заменяется предоставленным значением (bRef).

Все это приводит к поведению, описанному в сообщении, с обеими переменными (a и b), указывающими на bRef, где bRef.x является нулевым, а aRef.x указывает на bRef , что можно проверить с помощью дополнительной переменной, содержащей aRef как предложено @ Magnetron .

2 голосов
/ 29 мая 2020
• 1000 изменены):
public static void Main()
{
    MyClass myClass = new MyClass();
    MyClass x = new MyClass();
    myClass = (myClass.x = x);
    Console.WriteLine(myClass.x == myClass);
}

Итак, что происходит, a.x становится b, а затем b присваивается a. И локальная переменная a, и атрибут a.x теперь указывают на объект b. Итак:

  • a переменная указывает на b объект
  • b переменная указывает на b объект
  • a объект x атрибут указывает на b объект
  • b атрибут объекта x имеет значение null

Я немного изменил ваш код, чтобы лучше проиллюстрировать это:

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);
}

Этот код возвращает:

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

Только обратите внимание originalA теперь указывает на фактический a объект, другие локальные переменные теперь указывают на b.

Это не ошибка компилятора - см. ответ Магнетрона.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...