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

Ошибка

Странно видеть, как компилятор жалуется на изменение списка, когда вы можете изменить массив почти точно так же, так почему же нам не разрешено это делать?

Массив против списка

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

Множество

Вот код CIL, который обращается к массиву с индексом 0, а затем устанавливает свойство в “Abcd”:

Взгляните на блок 3 «Доступ и редактирование элемента». Вот краткое объяснение того, что делает каждая строка:

  • ldc.i4.0 — Помещает целочисленное значение 0 в стек как int32.
  • ldelema ValueTypeTest.SomeStruct — загружает адрес элемента массива по индексу, указанному int, который в данный момент находится на вершине стека (т. е. 0), на вершину стека.
  • ldstr “Abcd” — помещает новую ссылку на объект в “Abcd” в стеке.
  • call instance void ValueTypeTest.SomeStruct::set_SomeProp(string) — Вызывает метод установки свойств для свойства SomeProp, передающего строку, которая в данный момент находится на вершине стека (т. е. “Abcd”).

Вот как выглядит куча/стек. Свойство SomeProp устанавливается для экземпляра SomeStruct, расположенного в памяти 0x10, со строкой “Abcd”, расположенной в 0x20 (примечание: адреса памяти произвольны).

Как видите, свойство задано для фактического экземпляра SomeStruct (не копии), находящегося в массиве в памяти.

Теперь давайте сравним это со списком.

Список

Подобно массиву, когда вы вызываете someStructsList[0].SomeProp = “Abcd”;, он на самом деле делает две вещи: обращается к экземпляру SomeStruct, а затем устанавливает свойство.

Поскольку мы не можем скомпилировать someStructsList[0].SomeProp = “Abcd”, давайте разобьем его на следующий, который компилируется, и посмотрим, что он делает за кулисами:

Вот CIL:

Нас действительно интересует только код в оранжевом прямоугольнике.

ldc.i4.0 — загружает Int32 значение 0 в стек.

callvirt instance !0 class [mscorlib]System.Collections.Generic.List`1[valuetype ValueTypeTest.SomeStruct]::get_Item(int32) — вызывает метод индексатора.

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

Это критическая точка, вы получаете копию элемента, а не фактический элемент.

Вы, вероятно, уже знакомы с этой концепцией: когда метод возвращает экземпляр типа значения, вы получаете копию этого экземпляра, а не фактический экземпляр (если только вы не используете ref/out).

Вывод

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

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