Странное поведение приращения в C # - PullRequest
24 голосов
/ 02 июля 2011

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

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

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 и только затем увеличивает это значение для менянеожиданное поведение.Или это действительно ожидается, и я явно что-то упускаю?

Ответы [ 6 ]

21 голосов
/ 02 июля 2011

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.

В итоге, кажется, что target задания оценивается в первую очередь, а затем source .

Недурно до OP для действительно провокационного вопроса!

18 голосов
/ 03 июля 2011

Это хорошо определено в языке C # согласно Эрику Липперту и легко объяснимо.

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

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

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

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

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

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

  1. Левая сторона:
    1. newArray2 оценивается и результат запоминается
    2. IndTemp оценивается и результат запоминается
  2. Правая сторона:
    1. newArray1 оценивается и результат запоминается
    2. IndTemp оценивается и результат запоминается (но здесь 1)
    3. IndTemp увеличивается на 1
    4. Элемент массива извлекается путем индексации в массив из шага 2.1 и в индекс из шага 2.2
  3. Вернуться к левой стороне
    1. Элемент массива сохраняется путем индексации в массив с шага 1.1 на индекс с шага 1.2

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

У Эрика есть запись в блоге под названием Приоритет против порядка, редукция , которую следует прочитать.

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

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]
13 голосов
/ 02 июля 2011
newArray2[IndTmp] = newArray1[IndTmp++];

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

  1. newArray2 [0] = newArray1 [0]
  2. прирост
  3. newArray2 [1] = newArray1 [1]
  4. прибавка

и т. Д.

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

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

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
12 голосов
/ 05 июля 2011

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

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

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

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

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

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

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

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

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

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

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

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

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

3 голосов
/ 02 июля 2011

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

...