一、引用類型與值類型
值類型:直接存儲其值,派生自System.ValueType,部署在棧上。值類型不包含null,值類型在聲明後,不管是否已經賦值,編譯器會爲其分配內存。值類型通常在線程棧上分配(靜態分配)。
引用類型:存儲其值的引用,派生自Object類,部署在堆上。引用類型可以使用null,當聲明引用類型時,只會在棧上分配一小片內存,用於存放一個地址。當進行實例化的時候(new)會在堆上分配空間,並會把堆上空間的地址存儲到聲明時開闢的那個棧上的空間中(動態分配)。
二、常見的值類型引用類型
值類型:byte,sbyte,short,int,long,float,double,decimal,char,uint,ushort,ulong,bool ,枚舉類型,用戶定義的結構體struct
引用類型:class、delegate、dynamic、interface、object(Object)、string(String)、內插字符串(這個樣子的:$"Name = {name}, hours = {hours:hh}")。
特殊情況:
(1)數組。數組是引用類型,數組的對象在棧上。其每一個元素均分配在託管堆上。如果是值類型數組(int,float....)那麼會在堆上對數組自動進行初始化。如果是引用類型數組,那麼不會初始化任何元素。此時數組爲null。
(2)嵌套類型:如果聲明一個類,該類中包含值類型的字段,同時包含值類型的局部變量,那麼它同樣是引用類型。但是值類型字段和值類型局部變量位置是不同的。
例如:
class A
{
int a; //值類型字段
public A()
{
int b; //值類型局部變量
}
}
字段跟隨實例存儲,所以值類型字段a存儲在託管堆上。但是這裏的b是局部變量,它是值類型,它在棧上。
三、注意事項
1.值類型測試
using System;
namespace ValueAndReference
{
class Program
{
static void Main(string[] args)
{
int VT1, VT2; //值類型變量VT1,VT2
VT1 = 5;
VT2 = VT1;
VT1 = 6;
Console.WriteLine("VT1 is " + VT1 +" VT2 is " + VT2); //傳遞的只是值
}
}
}
這個沒啥可注意的,很正常。
2.引用類型測試(class)
using System;
namespace ValueAndReference
{
//引用類型定義
class ReferenceType
{
public int field; //定義字段
public ReferenceType(int v) //構造函數,初始化field
{
field = v;
}
}
class Program
{
static void Main(string[] args)
{
ReferenceType RT1 = new ReferenceType(5); //定義並實例化引用類型對象RT1,初始字段值爲5
ReferenceType RT2 = RT1;
Console.WriteLine("RT1 is " + RT1.field +",RT2 is " + RT2.field); //輸出RT1、RT2字段值,均爲5
RT2 = new ReferenceType(6); //實例化RT2,並初始字段值爲6
Console.WriteLine("RT1 is " + RT1.field +",RT2 is " + RT2.field); //輸出RT1字段值爲5、RT2字段值爲6
ReferenceType RT3 = new ReferenceType(5); //定義並實例化引用類型對象RT3,初始字段值爲5
ReferenceType RT4 = RT3;
Console.WriteLine("RT3 is "+RT3.field+",RT4 is "+RT4.field); //輸出RT3、RT4字段值,均爲5
RT4.field = 6; //更改RT3的字段值爲6
Console.WriteLine("RT3 is " + RT3.field +",RT4 is " + RT4.field); //輸出RT3、RT4字段值,均爲6
}
}
}
這個比較需要注意,RT1的field字段初始爲5,然後RT1賦值給RT2,由於RT2是引用變量,那麼修改RT2就是修改RT1,可是爲什麼RT1在後邊沒變化呢?這是因爲這種情況下RT1和RT2指向的不是一個堆的地址,具體可以看下邊的圖。RT3和RT4是正常的情況,修改了RT4的值,RT3同時跟着變化。關於這兩種情況其實很簡單,而且這個很好的反應了引用類型的性質。爲什麼第一種情況不行呢?是因爲第一種情況改變了引用對象本身,重新進行了實例化,那麼就是重新聲明瞭一個新的實例。而第二種情況改變的是對象的屬性,兩個對象還是對應的同一個實例。看下面兩幅圖:
第一種情況:
第一次實例化RT2時拷貝的RT1,此時RT1、RT2是一個對象。接下來的實例化是對RT2本身的改變,並且在這裏是會重新分配RT2地址的。所以這個RT2是一個全新的實例,與RT1沒有任何聯繫。所以它再怎麼改字段的值也不會在影響RT1的字段。
第二種情況:
這裏修改的只是字段,所以修改後並沒有產生新的實例,那麼RT1、RT2對應的還是一個實例。修改任何一個的字段值,另外一個會同時變化。
以上這些情況同樣適用於方法中的參數,如果在方法中重新實例了對象,那麼原始的對象在方法結束時不會做出任何改變,如果僅是改變對象實例的字段,那麼沒有任何問題,可以修改。如果想要在方法中對對象的實例本身做修改,使用ref傳遞參數,這個可以做到。
3.引用類型測試(string)
using System;
using System.Runtime.InteropServices;
namespace ValueAndReference
{
class Program
{
static void Main(string[] args)
{
string STR1 ="aaaaa";
string STR2 = STR1;
Console.WriteLine("STR1 is " + STR1+",STR2 is "+STR2); //輸出STT1、STR2
STR2 = "bbbbb";
Console.WriteLine("STR1 is " + STR1 +",STR2 is " + STR2); //輸出STT1、STR2
}
}
}
最後在說下這個string(String),在這裏STR1並沒有隨着STR2的變化而變化,這麼看起來它確實像值類型。但實際上它確實是引用類型。爲什麼出現這種情況那,這是因爲string做了運算符重載這樣看起來是string更像是字符串。可以把它這個賦值理解爲new,這樣就和上邊的class一樣了,所以這種情況是正常的。