Q1:
首先定義一個結構
public struct MyStruct { public int T; }
定義一個泛型List來存放結構體,然后訪問第一個元素去修改T,輸出T:
List<MyStruct> arrLis =new List<MyStruct>(){new MyStruct()};
arrLis[0].T = 100;
Console.WriteLine(arrLis[0].T);
大家猜是什么結果?
很遺憾不是100,arrLis[0].T = 100;VS提示該語句有錯誤。Cannot modify the expression because it is not a variable.
說修改的不是一個變量。
這是為什么呢?
關於這個問題我們首先來看一下List的源碼
其實List[]被稱做索引器。索引的實現其實類似屬性,靠一對Get,Set方法來實現的。索引器其實只是C#的語法糖而已。那么很明顯我們上面的語句其實只是調用了get_Item方法而已,且返回值MyStruct是個值類型。所以get_Item方法返回的是一個值(value)。你也許會說,那又怎么樣,我為什么就不能修改這個值。很不辛,在.NET中值(value)是不能被修改的,只有變量(variable)才能夠被修改,這就是為什么變量稱之為”變量”了:)。
Q2:
再看下面的代碼,我們修改一下,把泛型List改為Array數組。
MyStruct[] arrStr =new MyStruct[1]{new MyStruct()};
arrStr[0].T = 100;
Console.WriteLine(arrStr[0].T);
你是否覺得這次賦值語句也會報錯?
其實不然,代碼順利通過編譯,運行成功。
結果輸出:100
這太奇怪啦,為什么把List改成Array就沒有問題了呢。
讓我們繼續查看一下源碼
看到沒,對於一維數組的訪問其實是訪問到了這個GetValue方法。該方法的意思是使用typeReference去取到位於index位置的對象的引用,然后轉換為Object返回。看來原因就在這里了,對於數組的[]索引器其實是返回了對象的一個引用(地址),也就是相當於我們使用Array[0]訪問的是得到的是一個變量(variable),所以可以直接給內部的成員變量賦值。
對於這段源碼也許不是那么好理解,不妨看看IL。
ldelema:將位於指定數組索引的數組元素的地址作為 & 類型(托管指針)加載到計算堆棧的頂部。
這就很清楚了,在IL里面也清楚的顯示,操作的是對象的地址。
到這里,Array跟List索引訪問的區別出來了,Array是返回了對象的引用,而List返回的就是對象的值(值類型對象就是內部的值,引用類型對象是引用的地址)。
Q3:
還沒完,既然直接給賦值不行,那我用一個Set方法包裝起來,去設置內部變量的值如何?
public struct MyStruct {
public int T;
public void SetT(int t)
{ T = t; }
}
改造一下,加了一個SetT方法。
把List初始化語句也改一下,去掉一些語法糖,因為我們要查IL,語法糖會影響我們的判斷。
A:
List<MyStruct> arrLis = new List<MyStruct>();
var myStruct = new MyStruct();
arrLis.Add(myStruct);
arrLis[0].SetT(100);
Console.WriteLine(arrLis[0].T);
以上代碼順利通過。
輸出:0
那為什么直接訪問方法就可以呢。其實arrLis[0].SetT(100); 這也可以算是一個語法糖。上面A段代碼到了IL層面其實就相當於下面B段代碼,IL還是會用一個局部變量去接arrLis[0]返回的值。
B:
List<MyStruct> arrLis = new List<MyStruct>();
var myStruct = new MyStruct();
arrLis.Add(myStruct);
var temp = arrLis[0];
temp.SetT(100);
Console.WriteLine(arrLis[0].T);
不信我們查一下IL:
左邊是A段代碼,右邊是B段代碼:
這2段IL只有紅線畫出來的地方不一樣,其實就是一個變量命名不一樣而已。
Q4:
那上面A段代碼輸出為什么是0呢?
這個也很好理解,既然arrLis[0].SetT(100); 相當於var temp = arrLis[0]; 那么值類型賦值操作,其實是把右邊的值(副本)賦值給了左邊的變量,我們用SetT來修改T的時候只是在修改temp里面的T而已。這個不用多解釋吧。
總結:
當我們在List里面使用值類型的時候一定要格外小心,特別是使用結構體的時候,因為從表象上來說更像一個引用類型(結構可以定義方法,成員變量等),不知不覺你就會用引用類型對象的慣用法去處理問題,說不定就掉坑了。所以結構體最好定義為不可變的。
參考:
why-can-struct-change-their-own-fields