Array,List,Struct可能被你們忽略的問題

Q1:

首先定義一個結構html

public struct MyStruct { public int T; }express

定義一個泛型List來存放結構體,而後訪問第一個元素去修改T,輸出T:數組

List<MyStruct> arrLis =new List<MyStruct>(){new MyStruct()};post

arrLis[0].T = 100;spa

Console.WriteLine(arrLis[0].T);3d

你們猜是什麼結果?指針

很遺憾不是100,arrLis[0].T = 100;VS提示該語句有錯誤。Cannot modify the expression because it is not a variable.code

image

 

說修改的不是一個變量。htm

這是爲何呢?對象

關於這個問題咱們首先來看一下List的源碼

image

其實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就沒有問題了呢。

讓咱們繼續查看一下源碼

image

看到沒,對於一維數組的訪問實際上是訪問到了這個GetValue方法。該方法的意思是使用typeReference去取到位於index位置的對象的引用,而後轉換爲Object返回。看來緣由就在這裏了,對於數組的[]索引器實際上是返回了對象的一個引用(地址),也就是至關於咱們使用Array[0]訪問的是獲得的是一個變量(variable),因此能夠直接給內部的成員變量賦值。

對於這段源碼也許不是那麼好理解,不妨看看IL。

image

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段代碼:

image

這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

what-is-the-difference-between-listt-and-array-indexers

internals-of-array

相關文章
相關標籤/搜索