Array,List,Struct可能被大家忽略的問題


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.

image

 

說修改的不是一個變量。

這是為什么呢?

關於這個問題我們首先來看一下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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM