Приоритет-против-порядка возвращаются

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

int index = 0;
int value = this.arr[index++];

В итоге исполнения, value будет содержать то, что было в this.arr[0], а index будет равен 1, правильно?

Правильно. Теперь миф. Я часто слышу в качестве объяснения «поскольку ++ идет после переменной, то инкремент происходит после обращения к массиву».

Неправда! «После» подразумевает отношение, основанное на последовательности событий во времени, а логическая последовательность событий весьма точно определена. Порядок событий для этого фрагмента программы таков:

  1. Записать ноль в index
  2. Прочитать ссылку на this.arr и запомнить результат
  3. Прочитать значение index и запомнить результат
  4. Добавить единицу к результату шага 3 и запомнить результат
  5. Записать результат шага 4 в index
  6. Посмотреть значение по ссылке из шага 2 с индексом из шага 3 и запомнить результат
  7. Записать результат шага 6 в value

Я подчёркиваю, что точно определена именно логическая последовательность, поскольку компилятор С#, JIT-компилятор и процессор имеют право изменять фактический порядок событий в целях оптимизации, с тем ограничением, что оптимизированный результат должен быть неотличим от требуемого результата в однопоточных сценариях. Изменения порядка могут иногда наблюдаться в многопоточных сценариях. Анализ последствий этого факта безумно сложен. Возможно, я когда-нибудь напишу об этих сложностях, если осмелюсь. Но для обычных сценариев вы должны предполагать, что порядок событий во времени в точности следует правилам, указанным в спецификации.

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

int[] arr = {0};
int value = arr[arr[0]++];

Что происходит? Сначала мы читаем arr[0], который равен 0, и запоминаем это. Затем мы инкрементируем arr[0], так что arr[0] становится равным 1. Потом мы читаем значение arr[0], потому что мы запомнили 0, и получаем 1. Если бы мы выполняли инкремент после (внешней) индексации, то результат был бы нулём, потому что инкремент не произошёл бы до момента, пока мы не прочитаем значение.

-- Эрик в отпуске; эта статься была записана заранее.

оригинал статьи