在C#中,有兩種大類型——值類型和引用類型。
1、值類型與引用類型
深入的理解這兩種類型是非常重要的,面試官會考驗面試者對這兩個知識的了解來判斷基礎是否扎實,並且有沒有深入的去思考。
1.1 什麼是值類型與引用類型
值類型主要包括簡單類型、枚舉類型和結構體類型等。值類型的實例通常被分配到線程的堆棧上,變量保存的內容就是實例數據本身。
引用類型的實例則被分配到托管堆上,變量保存的是實例數據的內存地址。引用類型包括類類型、接口類型、委托類型和字符串類型等。
對於堆棧和托管堆,在前面將IL知識點的時候就已經有提及過的。可以把它們理解為內存中存儲數據的兩種結構。
下面的表格列出了C#中的基本類型
類別
說明
值類型
簡單類型
有符號整數:int、long、short、sbyte
無符號整數:uint、ulong、ushort、byte
字符類型:char
浮點型:float、double和高精度小數類型decimal
布爾類型:bool
枚舉類型
枚舉類型:enum
結構體類型
結構體類型:struct
引用類型
類類型
字符串類型:string
類類型:Console類和自己自定義的類類型
數組類型
一維數組和多維數據,如int[]與int[,](二維數組)
接口類型
由interface關鍵字定義的類型
委托類型
由delegate關鍵字定義的類型
1.2 值類型與引用類型的區別
這是面試中的重點,面試官經常問到的問題。
它們之間最大的區別在於——不同的內存分布。
值類型通常是被分配到線程的堆棧上的(並不是絕對的),而引用類型則被分配到托管堆上。不同的分配位置導致了不同的管理機制,值類型的管理由操作系統負責,而引用類型的管理則由垃圾回收器(GC)負責。
多說無益,先上個簡單的代碼說明一下:
class Program
{
static void Main(string[] args)
{
//valuetype是值類型
int valuetype=3;
//reftype是引用類型
string reftype="abc";
}
}
內存分布如下圖所示:
值類型與引用類型的區別就在於實際數據的存儲位置:值類型的變量和實際數據都存儲在堆棧中;而引用類型則只有變量存儲在堆棧中,變量存儲著實際數據的地址,實際數據存儲在與地址相對應的托管堆中。
Warning:
前面講過了,值類型通常是放在堆棧上,但這並不是絕對的。值類型實例不一定總會被分配到線程棧上。在引用類型中嵌套值類型時,或者在值類型裝箱的情況下,值類型的實例就會被分配到托管堆中。
嵌套結果包括值類型中嵌套定義了引用類型和引用類型中定義了值類型兩種情況。
1.2.1 引用類型中嵌套定義值類型
如果類的字段類型是值類型,它將作為引用類型實例的一部分,被分配到托管堆中。但那些作為局部變量的值類型,則仍然會被分配到線程堆棧中。
//引用類型嵌套定義值類型的情況
public class NestedValueTypeInRef
{
//valuetype作為引用類型的一部分被分配到托管堆上
private int valuetype=3;
public void method()
{
char c='c';//因為是方法中的局部變量,所以還是會存放在堆棧上。
}
}
class Program
{
static void Main(string[] args)
{
NestedValueTypeInRef reftype=new NestedValueTypeInRef();
}
}
還是以圖來解釋:
1.2.2 值類型中嵌套定義引用類型
值類型嵌套定義引用類型時,堆棧上將保存該引用類型的引用,而實際的數據則依然保存在托管堆中。
public class TestClass
{
public int x;
public int y;
}
//值類型嵌套定義引用類型的情況
public struct NestedRefTypeValue
{
//結構體字段,注意,結構體的字段不能被初始化
private TestClass classinValuetype;
//結構圖中的構造函數,注意,結構體中不能定義無參的構造函數
public NestedRefTypeInValue(TestClass t)
{
classinValuetype.x=3;
classinValuetype.y=5;
classinValuetype=t;
}
}
class Program
{
static void Main(string[] args)
{
NestedRefTypeInValue valuetype=new NestedRefTypeInValue(new TestClass());
}
}
以上代碼中的內存分配情況如下圖所示:
總結:
從以上兩個例子可以總結出:值類型實例總會被分配到它聲明的地方,聲明的是局部變量,將被分配到棧上,而聲明為引用類型成員時,則被分配到托管堆上;而引用類型實例總是分配到托管堆上。
上面值分析了值類型與引用類型在內存分布方面的區別,除此之外,還有以下幾個區別:
(1)值類型繼承自ValueType,ValueType又繼承自System.Object;而引用類型則直接繼承於System.Object.
(2)值類型的內存不受GC(垃圾回收器)控制,作用域結束時,值類型會被操作系統自行釋放,從而減輕了托管堆的壓力;而引用類型的內存管理則由GC來完成。所以與引用類型相比,值類型在性能方面更具優勢。
(3)值類型是密封的(sealed),你將不能把值類型作為其他任何類型的基類;而引用類型則一般具有繼承性,這裡指的是類和接口。
(4)值類型不能為null值,它會被默認初始化為數值0;而引用類型在默認的情況下會被初始化為null值,表示不指向托管堆中的任何地址。對值為null的引用類型的任何操作,都會引發NullReferenceException異常。
(5)由於值類型變量包含其實際數據,因此在默認情況下,值類型之間的參數傳遞不會影響變量本身;而引用類型變量保存的是數據的引用地址,它們作為參數被傳遞時,參數會發生改變,從而影響類型變量的值。
2、參數傳遞問題
在默認的情況下,C#方法中的參數傳遞都是按值進行的,但實際上參數傳遞的方式共有4種不同的情況,它們分別為:
(1)值類型參數的按值傳遞
(2)引用類型參數的按值傳遞
(3)值類型參數的按引用傳遞
(4)引用類型參數的按引用傳遞
2.1 值類型參數的按值傳遞
值類型的按值傳遞,傳遞的是該值類型實例的一個副本,也就是說形參接收到的是實參的一個副本,被調用方法操作的是實參的一個副本罷了。
class Program
{
static void Main(string[] args)
{
//1.值類型按值傳遞
Console.WriteLine("值類型按值傳遞的情況");
int addNum=1;
Add(addNum);
Console.WriteLine("調用方法後,實參addNum的值:"+addNum);
Console.ReadKey();
}
//1.值類型按值傳遞的情況
private static void Add(int addnum)
{
addnum=addnum+1;
Console.WriteLine("方法中addnum的值:"+addnum);
}
}
運行結果如下:
上圖從內存的角度說明了值類型參數按值傳遞的情況。
2.2 引用類型參數的按值傳遞
當傳遞的參數是引用類型時,傳遞和操作的目標是指向對象的地址,而傳遞的實際內容是對地址的復制。由於地址指向的是實參的值,當方法對地址進行操作時,實際上操作了地址所指向的值,所以調用方法後原來實參的值就會被修改。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("引用類型按值傳遞的情況");
RefClass refClass = new RefClass();
refClass.addNum = 1;
AddRef(refClass);
Console.WriteLine("調用方法後,實參addNum的值:"+refClass.addNum);
Console.ReadKey();
}
private static void AddRef(RefClass addnumRef)
{
addnumRef.addNum += 1;
Console.WriteLine("方法中addNum的值:"+addnumRef.addNum);
}
}
public class RefClass
{
public int addNum;
}
結果為:
2.3 string引用類型參數按值傳遞的特殊情況
這個是比較特殊的一種方式。string類型也是引用類型,然而在按值傳遞時,傳遞的參數卻不會因方法中形參的改變而被修改。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("String引用類型按值傳遞的特殊情況");
string str = "old string";
ChangeStr(str);
Console.WriteLine("調用方法後,實參str的值:"+ str);
Console.ReadKey();
}
private static void ChangeStr(string oldStr)
{
oldStr = "New String";
Console.WriteLine("方法中oldStr的值:"+oldStr);
}
}
按照前面對“引用類型參數按值傳遞”過程分析,這裡方法對字符串的修改會導致實參的值發生改變,然而實際運行結果並非如此。造成這個特殊性的原因是string具有不變性,一旦string類型被賦值,則它就是不可改變的,即不能通過代碼去修改它的值。
方法中oldStr="New String"代碼執行時,系統會重新分配一塊內存控件來存放New String字符串,然後把分配的內存首地址賦值給oldStr變量。所以,調用完方法後,str變量所指向的仍然是old string字符串,而oldStr變量則指向了New String字符串。
2.4 值類型和引用類型參數的按引用傳遞
不管是值類型還是引用類型,你都可以使用ref或out關鍵字來實現參數的按引用傳遞。並且在按引用進行傳遞時,方法的定義和調用都必須顯式地使用ref和out關鍵字,不可以將它們省略,否則會引起編譯錯誤。
還是用具體的代碼來說明:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("值類型和引用類型參數的按引用傳遞情況");
//num是值類型形參
int num = 1;
//refStr是引用類型實參
string refStr = "Old String";
ChangeByValue(ref num);
Console.WriteLine(num);
ChangeByStr(ref refStr);
Console.WriteLine(refStr);
Console.ReadKey();
}
private static void ChangeByStr(ref string numRef)
{
numRef = "new string";
Console.WriteLine(numRef);
}
private static void ChangeByValue(ref int numValue)
{
numValue = 10;
Console.WriteLine(numValue);
}
}
結果截圖:
從結果就可以看出,在值類型參數按引用傳遞的過程中,傳遞的是值類型變量的地址,其效果類似於引用類型的按值傳遞。不同的是,值類型參數按引用傳遞的地址是棧上值類型變量的地址,而引用類型按值傳遞的地址是變量所指向的托管堆中實際數據的地址。當方法對值類型變量的地址進行操作時,實現的是對值類型變量的實際數據的操作,所以改變了實參中的值。
而引用類型參數按引用傳遞的過程中,傳遞的是引用類型變量的地址,該地址是變量在堆棧上的地址,即傳遞的是引用的引用而不是引用本身。
總結:多動手,多思考。深入理解兩種類型的不同,對於面試的問題就會游刃有余。