本文介紹了值類型和引用類型在編程中的區別。值類型包括簡單類型、枚舉類型和結構體類型,通常被分配線上程的堆棧上,變數保存的是實例數據本身。引用類型實例則被分配在托管堆上,變數保存的是實例數據的記憶體地址。值類型由操作系統負責管理,而引用類型則由垃圾回收機制(GC)負責管理。本文還通過示例代碼展示了值類型... ...
1.值類型和引用類型
1.1 什麼是值類型和引用類型
- 值類型:包括簡單類型,枚舉類型,結構體類型等,值類型通常被分配線上程的堆棧上,變數保存的內容就是實例數據本身
- 引用類型:引用類型實例則被分配在托管堆上,變數保存的是實例數據的記憶體地址,引用類型主要包括類類型、介面類型、委托類型、字元串類型等
1.2 值類型和引用類型的區別
值類型和引用類型最主要的區別是——不同的記憶體分佈
我們之前介紹過,值類型分配在線程的堆棧上,引用類型分配在托管堆上,不同的分配位置導致了不同的管理機制,值類型由操作系統負責管理,引用類型則由垃圾回收機制(GC)負責管理
管理的主要是記憶體的分配與釋放
class Program {
static void Main(string[] args) {
// valuetype 是值類型
int valuetype = 3;
// reftype 是引用類型
string reftype = "abc";
}
}
在程式中,每個變數都有其堆棧地址,並且不同的變數,堆棧地址不同,valuetype 和 reftype 在堆棧地址占用了不同的位置,從下圖可以看出,無論是值類型還是引用類型,變數本身都是存儲在堆棧中,變數只是實例數據的一個引用
值類型的變數和實際數據通常會存儲線上程堆棧中,而引用類型則是,變數存儲線上程堆棧中,而實際數據存儲在托管堆中,此時變數存儲的是實際數據的地址,這個地址就像是我們實際生活中的地址,像快遞員,想要送包裹給你,也是需要你的地址
註意
:我們對值類型的說法是通常線上程堆棧中,而也有不在堆棧的情況
1.引用類型嵌套值類型
如果類的欄位類型是值類型,它作為引用類型的一部分,會被分配到托管堆中,但是局部變數的值類型,則仍會被分配到線程棧中
public class NestedValueTypeInRef {
// 這個和引用類型一起分配到托管堆中
private int valueType = 3;
public method() {
// 方法的局部變數分配到線程棧中
char c = 'c';
}
}
class Program {
static void Main(string[] agrs) {
NestedRefTypeInValue reftype = new NestedRefTypeInValue();
}
}
2.值類型嵌套定義引用類型
值類型嵌套定義引用類型,堆棧上將保存該引用類型的引用,而實際的數據則將保存在托管堆上
public class TestClass
{
public int x;
public int y;
}
public struct NestedRefTypeValue
{
// 註意結構體的欄位不能初始化
private TestClass classinValueType;
// 註意結構體的構造函數不能無參
public NestedRefTypeValue(TestClass t)
{
classinValueType.x = 3;
classinValueType.y = 5;
classinValueType = t;
}
}
class Program {
static void Main(string[] args) {
NestedRefTypeValue valueType = new NestedRefTypeInValue(new TestClass())
}
}
總結:
- 值類型繼承自ValueType,ValueType又繼承自System.Object;而引用類型則直接繼承自System.Object
- 值類型的記憶體不受GC控制,作用域結束,值類型會被系統自動釋放,從而減少了托管堆的壓力,而引用類型受到GC控制,所以與引用類型相比,值類型性能方面更占優勢
- 值類型是密封的(sealed),你不能把值類型作為其它任何類型的基類,而引用類型一般具有繼承性
- 值類型不能為null值,它的預設初始值為數值0,而由於類型在預設情況下為null值
- 由於值類型的變數包含其實際數據,因此在預設情況下,值類型之間的參數傳遞不會影響變數本身,而引用類型保存的數據的地址,它們作為參數傳遞,參數會發生變化,從而影響引用類型變數的值
1.3 兩大類型轉換——裝箱與拆箱
由於C#存在這兩種類型,自然需要對它們進行轉換,類型轉換指的是將數據的類型轉化為另外一種類型
類型轉換的方式:
- 隱式類型轉換:由低級類型向高級類型轉換的過程。例如:派生類可以隱式的轉換為它的父類,裝箱的過程就屬於指針隱式類型轉換
- 顯式類型轉換:強制類型轉換。這種轉換可能會導致精度丟失,或出現運行異常
強制類型轉換的格式
type就是你想要轉換的類型
(type)(變數、或函數)
- 通過
is
和as
運算符可以進行安全的類型轉換
if (myObj is MyClass)
{
// myObj 是 MyClass 類型的實例
}
MyClass myObj = someObj as MyClass;
if (myObj != null)
{
// someObj 成功轉換為 MyClass 類型
}
- 通過 .NET 類庫中的Convert類來完成類型轉換
string str = "123";
int num = Convert.ToInt32(str);
string str = "2023-07-19";
DateTime date = Convert.ToDateTime(str);
string str = "Red";
Color color = (Color)Enum.Parse(typeof(Color), str);
1.3.1 裝箱和拆箱的原理
class Program {
static void Main(string[] args) {
int i = 3;
// 裝箱操作
object o = i;
// 拆箱操作
int y = (int) o;
}
}
裝箱(box)可以具體為三個步驟
- 記憶體分配:在托管堆中分配好記憶體空間存放複製的實際數據
- 完成數據的複製:將值類型實例的實際數據複製到新分配的記憶體中
- 地址返回:將托管對象的地址返回給由於類型變數
拆箱(unbox)操作的步驟:
- 檢查實例:首先檢查要進行拆箱操作的引用類型變數是否為null,如果為null則拋出異常,如果不為null,則繼續檢查變數是否和拆箱之後的類型是否是同一個類型
- 地址返回:返回已裝箱變數的實際數據部分的地址
- 數據複製:將托管堆中的實際數據複製到棧中
註意
:
- 裝箱和拆箱堆性能有比較大的影響,如果代碼中有大量的裝箱和拆箱會消耗很多運行時間
- 裝箱和拆箱必然會產生多餘的對象,進一步加重了GC的壓力
應該避免多次的裝箱和拆箱,最好使用泛型來編程
2. 參數傳遞的問題
C# 中的參數傳遞,我們可以分為四種情況
- 值類型參數按值傳遞
- 引用類型的參數按值傳遞
- 值類型參數按引用傳遞
- 引用類型的參數按引用傳遞
2.1 值類型的參數按值傳遞
參數可以分為實參和形參兩種,形參指的是被調用方法中的參數,而實參指的是我們傳遞過去的參數
class Program {
static void Main(string[] args) {
int addNum = 1;
// addNum 就是實參
Add(addNum);
}
// addnum就是形參,即被調用方法中的參數
private static void Add(int addnum) {
addnum += 1;
Console.WriteLine(addnum);
}
}
值類型按值傳遞,其實傳遞的是該值類型實例的一個副本,也就是說,方法對參數的操作,並不會影響實際的參數
class Program {
static void Main(string[] args) {
int addNum = 1;
// addNum 就是實參
Add(addNum);
Console.WriteLine("調用方法之後的實際參數的值:"+addNum);
Console.Read();
}
// addnum就是形參,即被調用方法中的參數
private static void Add(int addnum) {
addnum += 1;
Console.WriteLine("方法中形參的值:"+addnum);
}
}
我們可以看到圖中,並沒有一根線將 addNum 與 addnum 進行綁定,addnum使用只是 addNum 的副本
2.2 引用類型按值傳遞
當傳遞的是引用類型的時,傳遞和操作的目標時指向對象的地址,而傳遞的實際內容對地址的複製,由於地址指向的是實際參數的值,當方法對地址進行操作的時候,實際上操作的地址所指向的實際值,當調用方法之後,實參也會被修改
public class RefClass
{
public int addNum;
}
class Program {
static void Main(string[] args) {
Console.WriteLine("引用類型按值傳遞的情況");
RefClass refClass = new RefClass();
refClass.addNum = 1;
AddRef(refClass);
Console.WriteLine("調用方法之後,實際參數的值:"+refClass.addNum);
Console.Read();
}
private static void AddRef(RefClass addnumRef) {
addNumRef.addNum += 1;
Console.WriteLine("方法中的addNum值:"+addNumRef.addNum);
}
}
2.3 string引用類型參數按值傳遞的特殊情況
雖然string類型也是引用類型,但是它按值傳遞,傳遞的參數斌不會因為方法中的形參改變而修改
這個特殊情況是因為string具有不變性,一旦string類型被賦值之後,則它就是不可改變的,即不能通過代碼修改它
2.4 值類型和引用類型的按引用傳遞
不管是值類型還是引用類型,都可以使用 ref 或 out 關鍵字來實現參數按引用傳遞,並且按引用進行傳遞的時候,方法的定義和調用都必須是顯式的使用 ref 或 out 關鍵字,不可省略
按引用傳遞時,不管是值類型,還是引用類型,本質都一樣是告訴編譯器,方法傳遞的是參數地址,而非參數本身
class Program
{
static void Main(string[] args)
{
// num 作為實際參數
int num = 1;
// refStr 是引用類型實參
string refStr = "Old string";
// 值類型按引用傳遞
ChangeByValue(ref num);
Console.WriteLine(num);
// 引用類型按引用傳遞
ChangeByRef(ref refStr);
Console.WriteLine(refStr);
Console.Read();
}
private static void ChangeByValue(ref int numValue)
{
numValue = 10;
Console.WriteLine(numValue);
}
private static void ChangeByRef(ref string numRef)
{
numRef = "new string";
Console.WriteLine(numRef);
}
}