В следния код съм създал прост масив и общ списък, като и двата съдържат екземпляр на 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 (nb адресите на паметта са произволни).

Както можете да видите, свойството е зададено за действителния екземпляр 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. Тъй като това копие не се съхранява никъде, вие просто задавате свойство на копие, което е на път да бъде изхвърлено, което вероятно не е това, което искате.

Ако го разделите на две стъпки с помощта на локална променлива, компилаторът не се оплаква, защото поне задавате свойство на копие на екземпляра, до което сега имате достъп в локална променлива.