Приоритет-против-порядка возвращаются
Еще раз я обращаюсь к мифу о том, что порядок вычисления в С# имеет какое-то отношение к приоритетам операторов. Вот вариант этого мифа, который я постоянно слышу. Предположим, у вас есть поле arr, являющееся массивом целых, и пара локальных переменных index и value:
int index = 0;
int value = this.arr[index++];
В итоге исполнения, value будет содержать то, что было в this.arr[0], а index будет равен 1, правильно?
Правильно. Теперь миф. Я часто слышу в качестве объяснения «поскольку ++ идет после переменной, то инкремент происходит после обращения к массиву».
Неправда! «После» подразумевает отношение, основанное на последовательности событий во времени, а логическая последовательность событий весьма точно определена. Порядок событий для этого фрагмента программы таков:
- Записать ноль в index
- Прочитать ссылку на this.arr и запомнить результат
- Прочитать значение index и запомнить результат
- Добавить единицу к результату шага 3 и запомнить результат
- Записать результат шага 4 в index
- Посмотреть значение по ссылке из шага 2 с индексом из шага 3 и запомнить результат
- Записать результат шага 6 в value
Я подчёркиваю, что точно определена именно логическая последовательность, поскольку компилятор С#, JIT-компилятор и процессор имеют право изменять фактический порядок событий в целях оптимизации, с тем ограничением, что оптимизированный результат должен быть неотличим от требуемого результата в однопоточных сценариях. Изменения порядка могут иногда наблюдаться в многопоточных сценариях. Анализ последствий этого факта безумно сложен. Возможно, я когда-нибудь напишу об этих сложностях, если осмелюсь. Но для обычных сценариев вы должны предполагать, что порядок событий во времени в точности следует правилам, указанным в спецификации.
Фактически, можно продемонстрировать, что инкремент происходит до индексирования при помощи этой маленькой самореферентной жемчужины:
int[] arr = {0};
int value = arr[arr[0]++];
Что происходит? Сначала мы читаем arr[0], который равен 0, и запоминаем это. Затем мы инкрементируем arr[0], так что arr[0] становится равным 1. Потом мы читаем значение arr[0], потому что мы запомнили 0, и получаем 1. Если бы мы выполняли инкремент после (внешней) индексации, то результат был бы нулём, потому что инкремент не произошёл бы до момента, пока мы не прочитаем значение.
-- Эрик в отпуске; эта статься была записана заранее.