C#中類型分為兩類:
- 值類型(Value Type)
- 引用類型(Reference Type)
值類型和引用類型是以它們?cè)谟?jì)算機(jī)內(nèi)存中是如何被分配的來劃分的。
值類型包括結(jié)構(gòu)和枚舉,值類型又包含一種特殊的值類型,稱為簡(jiǎn)單類型,如:int
byte
所有值類型都隱式(在C#代碼中,無法看到繼承關(guān)系,但通過MSIL代碼才可以看到)繼承自System.ValueType
,而System.ValueType
和引用類型都繼承自System.Object
基類
Tips:C#不支持多重繼承,結(jié)構(gòu) 已經(jīng)隱式繼承至System.ValueType,所以 結(jié)構(gòu) 不支持繼承。
什么是堆和棧?
堆和棧的概念:
- 棧(Stack)是一種后進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中,變量會(huì)被分配在棧上來進(jìn)行操作
- 堆(heap)是用于為引用類型的實(shí)例(對(duì)象),分配空間的內(nèi)存區(qū)域,在堆上創(chuàng)建一個(gè)對(duì)象,會(huì)將對(duì)象的地址傳給棧上的變量(反過來叫變量指向此對(duì)象,或者變量引用此對(duì)象)-----也就是棧上的變量指向了堆上地址為XXX的實(shí)例(對(duì)象)
值類型
public struct ValPoint{
public int x; // 該值類型中的字段
}
ValPoint vPoint1; // ValPoint:結(jié)構(gòu)值類型,vPoint1:變量,此時(shí)并沒有被壓到棧上
vPoint1.x = 10; // 進(jìn)行入棧操作
Console.WriteLine(vPoint1.x); //進(jìn)行出棧操作
變量包含了值類型中所有字段,該變量vPoint1 被分配在線程堆棧(Thread Stack)上。
Tips:只有在對(duì)變量操作時(shí),變量才會(huì)進(jìn)行入棧。對(duì)變量的操作,實(shí)際上是一系列的出棧和入棧操作。
結(jié)構(gòu)特點(diǎn)1:
定義的結(jié)構(gòu)內(nèi)所有字段都必須初始化賦值,否則會(huì)報(bào)出“使用了可能為賦值的字段x”.因?yàn)檫@是 .Net 的一個(gè)約束,所有的元素使用前都必須初始化,如:
int i;
Console.WriteLine(i); // 未初始化變量,使用結(jié)構(gòu)int中內(nèi)部成員在使用前都必須對(duì)它賦值
結(jié)構(gòu)特點(diǎn)2:
調(diào)用結(jié)構(gòu)內(nèi)的方法前,需要對(duì)結(jié)構(gòu)內(nèi)所有字段進(jìn)行賦值
// 修改ValPoint結(jié)構(gòu)
public struct ValPoint{
public int x;
public void Block( ){ }
}
//下面代碼將會(huì)編譯錯(cuò)誤
ValPoint vPoint1;
// vPoint1.x = 200; // 在聲明變量vPoint1后給結(jié)構(gòu)中x變量賦值,才能編譯通過
vPoint1.Block(); //使用了為賦值的局部變量vPoint1
Console.WriteLine(vPoint); //使用了為賦值的局部變量vPoint1
Tips:如果結(jié)構(gòu)中有多個(gè)字段,則都需要為多個(gè)字段進(jìn)行賦值
Q:上面例子中,結(jié)構(gòu)內(nèi)方法都沒有使用字段x,為什么還要進(jìn)行初始化賦值?這樣子后如果結(jié)構(gòu)中有若干個(gè)字段,初始化賦值豈不麻煩?
A:編譯器隱式地為結(jié)構(gòu)類型創(chuàng)建無參的夠著函數(shù),在這個(gè)構(gòu)造函數(shù)中會(huì)對(duì)結(jié)構(gòu)成員進(jìn)行初始化,所有值類型被賦予0或相當(dāng)于0的值,引用類型被賦予為null值(因此,Struct類型不可自行聲明無參數(shù)的構(gòu)造函數(shù)),所以通過隱式聲明的構(gòu)造函數(shù)去創(chuàng)建一個(gè)ValPoint類型變量:
ValPoint vPoint1 = new ValPoint();
Console.WriteLine(vPoint1.x); // 輸出為 0
引用類型
聲明一個(gè)引用類型變量,并使用new操作符創(chuàng)建引用類型實(shí)例的時(shí)候,該引用類型的變量會(huì)被分配到線程棧上,該變量只保存了位于堆上的引用類型的實(shí)例的內(nèi)存地址,變量本身不包含任何類型的數(shù)據(jù)。
僅僅定義了變量,沒有使用new操作符的話,由于沒有在堆上創(chuàng)建類型的實(shí)例,因此,該變量值為null,不指向任何對(duì)象(堆上對(duì)象的實(shí)例)
Tips:容易混淆的:變量(Variable)、對(duì)象(Object)、實(shí)例(Instance)。變量可以是值類型,也可以是引用類型,如果是引用類型的話,由于本身只包含對(duì)實(shí)例對(duì)象的引用(內(nèi)存地址),因此也叫對(duì)象引用;而在堆上創(chuàng)建的對(duì)象,稱為對(duì)象的實(shí)例。
public class RefPoint{
public int x;
public RefPoint(int x){this.x = x;}
public RefPoint(){}
}
僅寫下面一條聲明語句時(shí):
RefPoint rPoint1; // 在線程棧上創(chuàng)建一個(gè)不包含任何數(shù)據(jù),也不指向任何對(duì)象(不包含內(nèi)存地址)的變量
當(dāng)使用new操作符時(shí):
rPoint1 = new RefPoint(1);
- 在應(yīng)用程序堆上創(chuàng)建一個(gè)引用類型對(duì)象的實(shí)例,并為它分配內(nèi)存地址
- 自動(dòng)傳遞該實(shí)例的引用給構(gòu)造函數(shù)(正因如此,在構(gòu)造函數(shù)中才能使用this來訪問這個(gè)實(shí)例)
- 調(diào)用該類型的構(gòu)造函數(shù)
- 返回該實(shí)例的引用內(nèi)存地址,復(fù)制給 rPoint1 變量,該rPoint 引用對(duì)象保存的數(shù)據(jù)是指向在堆上創(chuàng)建改類型的實(shí)例地址。
簡(jiǎn)單類型
想比較兩個(gè)int類型是否相等,通常會(huì):
int i = 3;
int j = 3;
if(i == j){ Console.WriteLine("i equals to j");}
但是,對(duì)于自定義的引用類型,如結(jié)構(gòu),就不能使用 “==”來判斷它們是否相等,而需要 Equals() 方法來完成。
string 類型是引用類型,如果:
string a = "123";
string b = "123";
if(a == b){Console.WirteLine("a equals to b");}
其比較的是他們是否指向堆上同一個(gè)對(duì)象。上面顯然不同,但他們所包含的數(shù)值相同,所以,對(duì)string類型用 ‘==’比較,實(shí)際上是比較引用類型內(nèi)包含的值,而不是其引用。
裝箱和拆箱
- 裝箱: 值類型 -> 等價(jià)的引用類型
int i = 1;
Object boxed = i;
Console.WriteLine("Boxed Point:" + boxed);
- 在堆上為新的對(duì)象實(shí)例分配內(nèi)存。該對(duì)象實(shí)例包含數(shù)據(jù),但沒有名稱;
- 在棧上值類型變量的值復(fù)制到堆上的對(duì)象中
- 將堆上創(chuàng)建的對(duì)象的地址返回給引用類型變量
- 拆箱:將一個(gè)已裝箱的引用類型 -> 值類型
int i = 5;
Object boxed = i;
j = (int) boxed;
Console.WriteLine("UnBoxed Point : " + j);
- 獲取已裝箱的對(duì)象的地址
- 將值從堆上的對(duì)象中復(fù)制到堆棧上的值變量中
可見,裝箱與拆箱需要反復(fù)在堆上進(jìn)行操作,所以,在程序中盡量避免無意義的裝箱與拆箱。