裝箱/拆箱,實值型別/參考型別 和 Object類,這些都是.NET程式員人人皆知且人人都應該掌握的概念。大多數人都對他們非常瞭解,可是和一些同行們交流時我發現一些細節其實很多人並不瞭解,尤其是它們結合討論的情景,本文通過一些代碼來闡述一些我知道的概念。
目錄
- 代碼1:Object.Equals
- 代碼2:Object.ReferenceEquals
- 代碼3:再強化一下理解
- 代碼4:問候了Equals,我們再看看==
- 代碼5:神奇的String
- 代碼6:欄位和屬性
- 代碼7:MemberwiseClone()
返回目錄
代碼1:Object.Equals
考慮下面代碼的結果:
Console.WriteLine(Object.Equals(1, 1));
Console.WriteLine(Object.Equals(1, (byte)1));
答案:True, False
首先Object.Equals參數是兩個object,所以1(實值型別)會被裝箱成參考型別,這會使CLR在託管堆中建立兩個全新的Object對象,然後Object.Equals先判斷兩個object是否有null,沒有則調用Object的對象方法Equals,而由於1是實值型別,實值型別改寫Object.Equals並進行位元比較,最終由於object1的位元值完全等於object2的位元值第一句True,第二句很顯然Int和Byte記憶體大小不一樣,位元比較不會成功。
返回目錄
代碼2:Object.ReferenceEquals
代碼2:Object.ReferenceEquals
Console.WriteLine(Object.ReferenceEquals(1, 1));
答案:False
同樣,Object.ReferenceEquals參數是兩個object,所以CLR會在託管堆中建立兩個object來分別裝Int值,但Object.ReferenceEquals的函數就是判斷兩個引用的是否指向同一個在託管堆的空間對象,這裡當然是False了。
返回目錄
代碼3:再強化一下理解
下面代碼,如果MyType是class或struct時,分別會輸出什嗎?
struct/class MyType
{
public int Data;
}
class Program
{
static void Main(string[] args)
{
MyType s1 = new MyType();
MyType s2 = new MyType();
s1.Data = s2.Data = 1990;
Console.WriteLine(Object.Equals(s1, s2));
}
}
答案:
struct輸出:True
class輸出:False
這個為了強化下理解,原理和上面的一樣,實值型別和參考型別針對Object.Equals的執行是不一樣的
返回目錄
代碼4:問候了Equals,我們再看看==
下面代碼輸出什嗎?
struct MyType
{
public int Data;
}
class Program
{
static void Main(string[] args)
{
MyType s1 = new MyType();
MyType s2 = new MyType();
s1.Data = s2.Data = 1990;
Console.WriteLine(s1 == s2);
}
}
如果MyType是class,那麼結果所有人會知道是False,那如果MyType是struct,結果是?……結果是編譯錯誤,是的,實值型別中的使用者自訂結構體預設==運算子是不被預先重載的,但是參考型別,枚舉,原始實值型別的==有。
返回目錄
代碼5:神奇的String
下面輸出結果?
string a = "aaa";
string b = "aaa";
Console.WriteLine(Object.Equals(a, b));
Console.WriteLine(Object.ReferenceEquals(a, b));
Console.WriteLine(a == b);
Console.WriteLine((object)a == b);
//這句VS會提示警告:
//Possible unintended reference comparison; to get a value comparison,
//cast the left hand side to type 'string'
答案:都是True,但True的方式不一樣,呵呵,我們一句一句分析
第一句: 調用a.Equals(b),String類的執行是字串比較,true
第二句:注意這裡不進行字串比較,這裡是判斷兩個引用是不是指向同一個對象,因為Object.ReferenceEquals參數是兩個object,但是.NET中相同的字串(編譯器可預知判斷的)CLR會確保它們只向同一個記憶體空間,這個又稱字串的Interning。
第三句:直接調用String的重載==,字串比較。
第四句:調用參考型別(Object)的重載==,其實等於調用Object.ReferenceEquals。參考第二句,這裡Visual Studio提示警告也驗證了第二句的結論,這裡不會進行字串比較,而是判斷兩個引用是否指向同一片記憶體空間對象。
返回目錄
代碼6:欄位和屬性
考慮如果Point是class或struct下面程式的結果?
struct/class Point
{
public int X, Y;
}
class MyCls
{
public Point PField;
public Point PProperty { get; set; }
}
class Program
{
static void Main()
{
MyCls cls = new MyCls();
cls.PField.X = 3;
cls.PProperty.X = 3;
}
}
答案:如果是Point是struct(即實值型別),cls.PField.X會賦值成功,而cls.FProperty.X不會賦值成功(其實根本無法編譯成功),因為屬性本質上就是函數調用,這裡PProperty返回一個實值型別的拷貝,編輯這個拷貝的內部欄位是沒有意義的。
如果Point是class(即參考型別),會拋出NullReference異常,因為類內的參考型別預設CLR不為他們分配空間的,所以他們保持預設值(null)。
返回目錄
代碼7:MemberwiseClone()
MemberwiseClone()是一個非常有用的函數,但很多人不會用它,它不是引用的直接拷貝,而是將成員欄位進行複製,如果成員是實值型別,那麼將進行深層拷貝,如果是參考型別,那麼只拷貝引用指標(前後兩個引用指向託管堆中的同一份空間)。
考慮下面代碼輸出?
class a
{
public object obj;
public object ShadowCopy()
{
return MemberwiseClone();
}
}
class Program
{
static void Main(string[] args)
{
a oa = new a() { obj = new object() };
a ob = oa;
a oc = (a)oa.ShadowCopy();
oa.obj = null;
Console.WriteLine(ob.obj == null);
Console.WriteLine(oc.obj == null);
}
}
答案:True, False
ob和oa指向同一個對象,所以oa變了,ob也變,oc是oa的MemberwiseClone的結果,oa的改變僅將自己的引用改成null。而oc沒變,oc的成員引用還指向原來的位置。