Подтвердить что ты не робот

Странное приращение поведения в С#

Примечание: Обратите внимание, что приведенный ниже код по существу не имеет смысла и просто для иллюстрации.

Исходя из того факта, что правая часть присваивания всегда должна быть оценена до того, как ей присваивается значение левой стороне переменной, а операции с приращением, такие как ++ и --, всегда выполняются правильно после оценки я не ожидал, что следующий код будет работать:

string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];

int IndTmp = 0;

foreach (string TmpString in newArray1)
{
    newArray2[IndTmp] = newArray1[IndTmp++];
}

Скорее, я ожидал бы, что newArray1[0] будет присвоен newArray2[1], newArray1[1] - newArray[2] и т.д. до точки бросания System.IndexOutOfBoundsException. Вместо этого, и к моему большому удивлению, версия, которая генерирует исключение, составляет

string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];

int IndTmp = 0;

foreach (string TmpString in newArray1)
{
    newArray2[IndTmp++] = newArray1[IndTmp];
}

Так как, по моему мнению, компилятор сначала оценивает RHS, присваивает его LHS и только затем увеличивает это, это для меня неожиданное поведение. Или это действительно ожидаемо, и я явно что-то пропустил?

4b9b3361

Ответ 1

Поучительно видеть, где именно ваша ошибка:

правая часть присваивания всегда должна быть оценена до того, как значение будет присвоено левой стороне переменной

Правильно. Очевидно, что побочный эффект назначения не может произойти до тех пор, пока не будет вычислено назначенное значение.

операции приращения, такие как ++ и - всегда выполняются сразу после оценки

Почти правильно. Неясно, что вы подразумеваете под "оценкой" - оценка чего? Исходное значение, добавочное значение или значение выражения? Самый простой способ подумать об этом заключается в том, что исходное значение вычисляется, затем увеличивается значение, затем происходит побочный эффект. Тогда конечное значение - это то, что выбрано одно из исходного или добавочного значения, в зависимости от того, был ли оператор префиксным или постфиксным. Но ваша основная предпосылка довольно хороша: побочный эффект приращения происходит сразу после определения конечного значения, а затем получается окончательное значение.

Затем вы, кажется, завершаете ложь из этих двух правильных предпосылок, а именно, что побочные эффекты левой стороны возникают после оценки правой стороны. Но ничто в этих двух предпосылках не подразумевает такого вывода! Вы просто вытащили этот вывод из воздуха.

Было бы более ясно, если бы вы указали третье правильное предположение:

место хранения, связанное с левой переменной, также должно быть известно до того, как произойдет присвоение.

Ясно, что это правда. Вам нужно знать две вещи, прежде чем может произойти назначение: какое значение назначается, и какое место памяти мутируется. Вы не можете понять эти две вещи одновременно; вы должны сначала выяснить один из них, и мы разобраем тот, который находится слева - переменная - сначала на С#. Если выяснить, где находится хранилище, возникает побочный эффект, тогда этот побочный эффект возникает, прежде чем мы выясним второе - значение, присвоенное переменной.

Короче говоря, в С# порядок оценок при присваивании переменной выглядит следующим образом:

  • возникают побочные эффекты левой стороны и создается переменная
  • возникают побочные эффекты правой стороны и создается значение
  • значение неявно преобразуется в тип левой стороны, что может вызвать третий побочный эффект.
  • возникает побочный эффект присваивания - происходит мутация переменной, чтобы иметь значение правильного типа, и создается значение - значение, только что назначенное левой стороне.

Ответ 2

ILDasm может быть вашим лучшим другом, иногда; -)

Я собрал оба ваших метода и сравнил полученный IL (язык ассемблера).

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

Code         Description                  Stack
ldloc.1      Load ref to newArray2        newArray2
ldloc.2      Load value of IndTmp         newArray2,0
ldloc.0      Load ref to newArray1        newArray2,0,newArray1
ldloc.2      Load value of IndTmp         newArray2,0,newArray1,0
dup          Duplicate top of stack       newArray2,0,newArray1,0,0
ldc.i4.1     Load 1                       newArray2,0,newArray1,0,0,1
add          Add top 2 values on stack    newArray2,0,newArray1,0,1
stloc.2      Update IndTmp                newArray2,0,newArray1,0     <-- IndTmp is 1
ldelem.ref   Load array element           newArray2,0,"1"
stelem.ref   Store array element          <empty>                     
                                                  <-- newArray2[0] = "1"

Это повторяется для каждого элемента newArray1. Важным моментом является то, что местоположение элемента в исходном массиве было перенесено в стек до того, как IndTmp будет увеличен.

Сравните это со вторым методом:

Code         Description                  Stack
ldloc.1      Load ref to newArray2        newArray2
ldloc.2      Load value of IndTmp         newArray2,0
dup          Duplicate top of stack       newArray2,0,0
ldc.i4.1     Load 1                       newArray2,0,0,1
add          Add top 2 values on stack    newArray2,0,1
stloc.2      Update IndTmp                newArray2,0     <-- IndTmp is 1
ldloc.0      Load ref to newArray1        newArray2,0,newArray1
ldloc.2      Load value of IndTmp         newArray2,0,newArray1,1
ldelem.ref   Load array element           newArray2,0,"2"
stelem.ref   Store array element          <empty>                     
                                                  <-- newArray2[0] = "2"

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

Для полноты сравним его с

newArray2[IndTmp] = newArray1[++IndTmp];

Code         Description                  Stack
ldloc.1      Load ref to newArray2        newArray2
ldloc.2      Load IndTmp                  newArray2,0
ldloc.0      Load ref to newArray1        newArray2,0,newArray1
ldloc.2      Load IndTmp                  newArray2,0,newArray1,0
ldc.i4.1     Load 1                       newArray2,0,newArray1,0,1
add          Add top 2 values on stack    newArray2,0,newArray1,1
dup          Duplicate top stack entry    newArray2,0,newArray1,1,1
stloc.2      Update IndTmp                newArray2,0,newArray1,1  <-- IndTmp is 1
ldelem.ref   Load array element           newArray2,0,"2"
stelem.ref   Store array element          <empty>                     
                                                  <-- newArray2[0] = "2"

Здесь результат инкремента был перенесен в стек (и становится индексом массива) до обновления IndTmp.

Итак, кажется, что сначала оценивается цель назначения, затем источник.

Пытается до ОП для действительно заставляющего думать вопроса!

Ответ 3

Это хорошо определено на языке С# в соответствии с Eric Lippert и легко объясняется.

  • Вычисляется первое выражение в левом порядке, на которое нужно ссылаться и запоминаться, и учитываются побочные эффекты.
  • Затем выполняется выражение в правильном порядке

Примечание. Фактическое выполнение кода может быть не таким, важно помнить, что компилятор должен создать эквивалент для этого

Итак, что происходит во втором фрагменте кода: 

  • Левая сторона:
    • newArray2 оценивается, и результат запоминается (т.е. ссылка на любой массив, который мы хотим хранить в памяти, запоминается, если последующие изменения впоследствии изменяются)
    • IndTemp оценивается и результат запоминается
    • IndTemp увеличивается на 1
  • Правая сторона:
    • newArray1 оценивается и результат запоминается
    • IndTemp оценивается, и результат запоминается (но здесь 1)
    • Элемент массива извлекается путем индексирования в массив с шага 2.1 по индексу с шага 2.2
  • Назад в левую сторону
    • Элемент массива хранится путем индексирования в массив с шага 1.1 по индексу с шага 1.2

Как вы можете видеть, второй раз IndTemp оценивается (RHS), значение уже увеличено на 1, но это не влияет на LHS, поскольку оно запоминает, что значение было увеличено до 0.

В первом фрагменте кода порядок немного отличается:

  • Левая сторона:
    • newArray2 оценивается и результат запоминается
    • IndTemp оценивается и результат запоминается
  • Правая сторона:
    • newArray1 оценивается и результат запоминается
    • IndTemp оценивается, и результат запоминается (но здесь 1)
    • IndTemp увеличивается на 1
    • Элемент массива извлекается путем индексирования в массив с шага 2.1 по индексу с шага 2.2
  • Назад в левую сторону
    • Элемент массива хранится путем индексирования в массив с шага 1.1 по индексу с шага 1.2

В этом случае увеличение переменной на шаге 2.3 не влияет на текущую итерацию цикла, и поэтому вы всегда будете копировать из индекса N в индекс N, тогда как во втором фрагменте кода вы будете всегда копируйте из индекса N+1 в индекс N.

У Эрика есть запись в блоге под названием Приоритет vs order, redux, который следует прочитать.

Вот фрагмент кода, который иллюстрирует, я в основном превращал переменные в свойства класса и реализовал пользовательскую коллекцию "массив", которая просто сбрасывает на консоль то, что происходит.

void Main()
{
    Console.WriteLine("first piece of code:");
    Context c = new Context();
    c.newArray2[c.IndTemp] = c.newArray1[c.IndTemp++];

    Console.WriteLine();

    Console.WriteLine("second piece of code:");
    c = new Context();
    c.newArray2[c.IndTemp++] = c.newArray1[c.IndTemp];
}

class Context
{
    private Collection _newArray1 = new Collection("newArray1");
    private Collection _newArray2 = new Collection("newArray2");
    private int _IndTemp;

    public Collection newArray1
    {
        get
        {
            Console.WriteLine("  reading newArray1");
            return _newArray1;
        }
    }

    public Collection newArray2
    {
        get
        {
            Console.WriteLine("  reading newArray2");
            return _newArray2;
        }
    }

    public int IndTemp
    {
        get
        {
            Console.WriteLine("  reading IndTemp (=" + _IndTemp + ")");
            return _IndTemp;
        }

        set
        {
            Console.WriteLine("  setting IndTemp to " + value);
            _IndTemp = value;
        }
    }
}

class Collection
{
    private string _name;

    public Collection(string name)
    {
        _name = name;
    }

    public int this[int index]
    {
        get
        {
            Console.WriteLine("  reading " + _name + "[" + index + "]");
            return 0;
        }

        set
        {
            Console.WriteLine("  writing " + _name + "[" + index + "]");
        }
    }
}

Выход:

first piece of code:
  reading newArray2
  reading IndTemp (=0)
  reading newArray1
  reading IndTemp (=0)
  setting IndTemp to 1
  reading newArray1[0]
  writing newArray2[0]

second piece of code:
  reading newArray2
  reading IndTemp (=0)
  setting IndTemp to 1
  reading newArray1
  reading IndTemp (=1)
  reading newArray1[1]
  writing newArray2[0]

Ответ 4

newArray2[IndTmp] = newArray1[IndTmp++];

приводит к сначала анализу, а затем добавлению переменной.

  • newArray2 [0] = newArray1 [0]
  • приращение
  • newArray2 [1] = newArray1 [1]
  • приращение

и т.д.

Оператор RHS ++ сразу увеличивается, но он возвращает значение до того, как оно было увеличено. Значение, используемое для индексации в массиве, - это значение, возвращаемое оператором RHS ++, поэтому значение без инкремента.

То, что вы описали (исключение выбрано), будет результатом LHS ++:

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception

Ответ 5

Очевидно, что предположение о том, что rhs всегда оценивается до того, как lhs ошибочно. Если вы посмотрите здесь http://msdn.microsoft.com/en-us/library/aa691315(v=VS.71).aspx, похоже, что в случае доступа индексатора оцениваются аргументы выражения доступа индексатора, который является lhs, перед rhs.

Другими словами, сначала определяется, где хранить результат rhs, только тогда оценивается rhs.

Ответ 6

Он выдает исключение, потому что вы начинаете индексирование в newArray1 в индексе 1. Поскольку вы выполняете итерацию по каждому элементу в newArray1, последнее присваивание вызывает исключение, поскольку IndTmp равно newArray1.Length, т.е. один мимо конца массива. Вы увеличиваете индексную переменную до того, как она когда-либо используется для извлечения элемента из newArray1, что означает, что вы потерпите крах, а также пропустите первый элемент в newArray1.