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