程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#值類型和援用類型的深刻懂得

C#值類型和援用類型的深刻懂得

編輯:C#入門知識

C#值類型和援用類型的深刻懂得。本站提示廣大學習愛好者:(C#值類型和援用類型的深刻懂得)文章只能為提供參考,不一定能成為您想要的結果。以下是C#值類型和援用類型的深刻懂得正文


從概念上看,值類型直接存儲其值,而援用類型存儲對其值的援用。這兩品種型存儲在內存的分歧處所。在C#中,我們必需在設計類型的時刻就決議類型實例的行動。這類決議異常主要,用《CLR via C#》作者Jeffrey Richter的話來 說,“不睬解援用類型和值類型差別的法式員將會給代碼引入詭異的bug和機能成績(I believe that a developer who misunderstands the difference between reference types and value types will introduce subtle bugs and performance issues into their code.)”。這就請求我們准確懂得和應用值類型和援用類型。

1. 通用類型體系

C#中,變量是值照樣援用僅取決於其數據類型。
C#的根本數據類型都以平台有關的方法來界說。C#的預界說類型並沒有內置於說話中,而是內置於.NET Framework中。.NET應用通用類型體系(CTS)界說了可以在中央說話(IL)中應用的預界說數據類型,一切面向.NET的說話都終究被編譯為 IL,即編譯為基於CTS類型的代碼。

例如,在C#中聲明一個int變量時,聲明的現實上是CTS中System.Int32的一個實例。這具有主要的意義:
確保IL上的強迫類型平安;
完成了分歧.NET說話的互操作性;
一切的數據類型都是對象。它們可以無方法,屬性,等。例如:
int i;
i = 1;
string s;
s = i.ToString();

MSDN的這張圖解釋了CTS中各個類型是若何相干的。留意,類型的實例可以只是值類型或自描寫類型,即便這些類型有子種別也是如斯。




2. 值類型

C#的一切值類型均隱式派生自System.ValueType:
構造體:struct(直接派生於System.ValueType);

數值類型:
整 型:sbyte(System.SByte的別號),short(System.Int16),int(System.Int32),long (System.Int64),byte(System.Byte),ushort(System.UInt16),uint (System.UInt32),ulong(System.UInt64),char(System.Char);
浮點型:float(System.Single),double(System.Double);
用於財政盤算的高精度decimal型:decimal(System.Decimal)。
bool型:bool(System.Boolean的別號);
用戶界說的構造體(派生於System.ValueType)。
列舉:enum(派生於System.Enum);
可空類型(派生於System.Nullable<T>泛型構造體,T?現實上是System.Nullable<T>的別號)。

每種值類型均有一個隱式的默許結構函數來初始化該類型的默許值。例如:
int i = new int();

等價於:
Int32 i = new Int32();

等價於:
int i = 0;

等價於:
Int32 i = 0;

應用new運算符時,將挪用特定類型的默許結構函數並對變量賦以默許值。在上例中,默許結構函數將值0賦給了i。MSDN上有完全的默許值表。

關於int和Int32的細節,在我的另外一篇文章中有具體說明:《懂得C#中的System.Int32和int》。

一切的值類型都是密封(seal)的,所以沒法派生出新的值類型。

值得留意的是,System.ValueType直接派生於System.Object。即System.ValueType自己是一個類類型,而 不是值類型。其症結在於ValueType重寫了Equals()辦法,從而對值類型依照實例的值來比擬,而不是援用地址來比擬。

可以用Type.IsValueType屬性來斷定一個類型能否為值類型:

TestType testType = new TestType ();
if (testTypetype.GetType().IsValueType)
{
Console.WriteLine("{0} is value type.", testType.ToString());
}

3. 援用類型

C#有以下一些援用類型:
數組(派生於System.Array)
用戶用界說的以下類型:
類:class(派生於System.Object);
接口:interface(接口不是一個“器械”,所以不存在派生於何處的成績。Anders在《C# Programming Language》中說,接口只是表現一種商定[contract]);
拜托:delegate(派生於System.Delegate)。
object(System.Object的別號);
字符串:string(System.String的別號)。

可以看出:
援用類型與值類型雷同的是,構造體也能夠完成接口;
援用類型可以派生出新的類型,而值類型不克不及;
援用類型可以包括null值,值類型不克不及(可空類型功效許可將 null 賦給值類型);
援用類型變量的賦值只復制對對象的援用,而不復制對象自己。而將一個值類型變量賦給另外一個值類型變量時,將復制包括的值。

關於最初一條,常常混雜的是string。我已經在一本書的一個晚期版本上看到String變量比string變量效力高;我還常常據說String是援用類型,string是值類型,等等。例如:
string s1 = "Hello, ";
string s2 = "world!";
string s3 = s1 + s2;//s3 is "Hello, world!"


這確切看起來像一個值類型的賦值。再如:
string s1 = "a";
string s2 = s1;
s1 = "b";//s2 is still "a"


轉變s1的值對s2沒有影響。這更使string看起來像值類型。現實上,這是運算符重載的成果,當s1被轉變時,.NET在托管堆上為s1從新分派了內存。如許的目標,是為了將做為援用類型的string完成為平日語義下的字符串。

4. 值類型和援用類型在內存中的安排

常常據說,而且常常在書上看到:值類型安排在棧上,援用類型安排在托管堆上。現實上並沒有這麼簡略。

MSDN上說:托管堆上安排了一切援用類型。這很輕易懂得。當創立一個運用類型變量時:
object reference = new object();

症結字new將在托管堆上分派內存空間,並前往一個該內存空間的地址。右邊的reference位於棧上,是一個援用,存儲著一個內存地址;而這個 地址指向的內存(位於托管堆)裡存儲著其內容(一個System.Object的實例)。上面為了便利,簡稱援用類型安排在托管推上。

再來看值類型。《C#說話標准》 上的措辭是“構造體不請求在堆上分派內存(However, unlike classes, structs are value types and do not require heap allocation)”而不是“構造體在棧上分派內存”。這難免輕易讓人覺得迷惑:值類型畢竟安排在甚麼處所?
4.1 數組

斟酌數組:
int[] reference = new int[100];

依據界說,數組都是援用類型,所以int數組固然是援用類型(即reference.GetType().IsValueType為false)。

而int數組的元素都是int,依據界說,int是值類型(即reference[i].GetType().IsValueType為true)。那末援用類型數組中的值類型元素畢竟位於棧照樣堆?

假如用WinDbg去看reference[i]在內存中的詳細地位,就會發明它們其實不在棧上,而是在托管堆上。

現實上,關於數組:
TestType[] testTypes = new TestType[100];

假如TestType是值類型,則會一次在托管堆上為100個值類型的元素分派存儲空間,並主動初始化這100個元素,將這100個元素存儲到這塊內存裡。

假如TestType是援用類型,則會先在托管堆為testTypes分派一次空間,而且這時候不會主動初始化任何元素(即testTypes[i]均為null)。比及今後有代碼初始化某個元素的時刻,這個援用類型元素的存儲空間才會被分派在托管堆上。

4.2 類型嵌套

更輕易讓人迷惑的是援用類型包括值類型,和值類型包括援用類型的情形:

public class ReferenceTypeClass
{
private int _valueTypeField;
public ReferenceTypeClass()
{
_valueTypeField = 0;
}
public void Method()
{
int valueTypeLocalVariable = 0;
}
}
ReferenceTypeClass referenceTypeClassInstance = new ReferenceTypeClass();//Where is _valueTypeField?
referenceTypeClassInstance.Method();//Where is valueTypeLocalVariable?

public struct ValueTypeStruct
{
private object _referenceTypeField;
public ValueTypeStruct()
{
_referenceTypeField = new object();
}
public void Method()
{
object referenceTypeLocalVariable = new object();
}
}
ValueTypeStruct valueTypeStructInstance = new ValueTypeStruct();//Where is _referenceTypeField?
valueTypeStructInstance.Method();//Where is referenceTypeLocalVariable?

單看valueTypeStructInstance,這是一個構造體實例,感到仿佛是整塊扔到棧上的。然則字段_referenceTypeField是援用類型,部分變量referenceTypeLocalVarible也是援用類型。

referenceTypeClassInstance也有異樣的成績,referenceTypeClassInstance自己是援用類型,似 乎應當整塊安排在托管堆上。但字段_valueTypeField是值類型,部分變量valueTypeLocalVariable也是值類型,它們畢竟 是在棧上照樣在托管堆上?

紀律是:
援用類型安排在托管堆上;
值類型老是分派在它聲明的處所:作為字段時,追隨其所屬的變量(實例)存儲;作為部分變量時,存儲在棧上。

我們來剖析一下下面的代碼。關於援用類型實例,即referenceTypeClassInstance:
從高低文看,referenceTypeClassInstance是一個部分變量,所以安排在托管堆上,並被棧上的一個援用所持有;
值類型字段_valueTypeField屬於援用類型實例referenceTypeClassInstance的一部門,所以追隨援用類型實例referenceTypeClassInstance安排在托管堆上(有點相似於數組的情況);
valueTypeLocalVariable是值類型部分變量,所以安排在棧上。

而關於值類型實例,即valueTypeStruct:
依據高低文,值類型實例valueTypeStructInstance自己是一個部分變量而不是字段,所以位於棧上;
其援用類型字段_referenceTypeField不存在追隨的成績,必定安排在托管堆上,並被一個援用所持有(該援用是valueTypeStruct的一部門,位於棧);
其援用類型部分變量referenceTypeLocalVariable明顯安排在托管堆上,並被一個位於棧的援用所持有。

所以,簡略地說“值類型存儲在棧上,援用類型存儲在托管堆上”是纰謬的。必需詳細情形詳細剖析。

5. 准確應用值類型和援用類型

這一部門重要參考《Effective C#》,並不是自己原創,願望能讓你加深對值類型和援用類型的懂得。
5.1 辨明值類型和援用類型的應用場所

C#中,我們用struct/class來聲明一個類型為值類型/援用類型。

斟酌上面的例子:
TestType[] testTypes = new TestType[100];

假如TestTye是值類型,則只須要一次分派,年夜小為TestTye的100倍。而假如TestTye是援用類型,剛開端須要100次分派,分派 後數組的各元素值為null,然後再初始化100個元素,成果總共須要停止101次分派。這將消費更多的時光,形成更多的內存碎片。所以,假如類型的職責 重要是存儲數據,值類型比擬適合。

普通來講,值類型(不支撐多態)合適存儲供 C#運用法式操作的數據,而援用類型(支撐多態)應當用於界說運用法式的行動。

平日我們創立的援用類型老是多於值類型。假如以下成績的答復都為yes,那末我們就應當創立為值類型:
該類型的重要職責能否用於數據存儲?
該類型的共有托言能否完整由一些數據成員存取屬性界說?
能否確信該類型永久弗成能有子類?
能否確信該類型永久弗成能具有多態行動?

5.2 將值類型盡量完成為具有常量性和原子性的類型

具有常量性的類型很簡略:
假如結構的時刻驗證了參數的有用性,以後就一向有用;
省去了很多毛病檢討,由於制止更改;
確保線程平安,由於多個reader拜訪到異樣的內容;
可以平安地裸露給外界,由於挪用者不克不及更改對象的外部狀況。

具有原子性的類型都是單一的實體,我們平日會直代替換一個原子類型的全部內容。

上面是一個典范的可變類型:

public struct Address
{
private string _city;
private string _province;
private int _zipCode;
public string City
{
get { return _city; }
set { _city = value; }
}
public string Province
{
get { return _province; }
set
{
ValidateProvince(value);
_province = value;
}
}
public int ZipCode
{
get { return _zipCode; }
set
{
ValidateZipCode(value);
_zipCode = value;
}
}
}

上面創立一個實例:
Address address = new Address();
address.City = "Chengdu";
address.Province = "Sichuan";
address.ZipCode = 610000;


然後更改這個實例:
address.City = "Nanjing"; //Now Province and ZipCode are invalid
address.ZipCode = 210000; //Now Province is still invalid
address.Province = "Jiangsu";

可見,外部狀況的轉變意味著能夠違背對象的不變式(invariant),至多是暫時的違背。假如下面是一個多線程的法式,那末在 City更改的進程中,另外一個線程能夠看到紛歧致的數據視圖。假如不是多線程的法式,也有成績:
當ZipCode的值有效而拋出異常時,對象僅作了一部門轉變,是以處於有效的狀況,為了修復這個成績,須要在Address中添加相當多的外部校驗代碼;
為了完成異常平安,我們須要在一切轉變多個字段的客戶代碼處放上進攻性的代碼;
線程平安也請求我們在每個屬性的拜訪器上添加線程同步檢討。

明顯,這是一個相當可不雅的任務量。上面我們把Address完成為常量類型:

public struct Address
{
private string _city;
private string _province;
private int _zipCode;
public Address (string city, string province, int zipCode)
{
_city = city;
_province = province;
_zipCode = zipCode;
ValidateProvince(province);
ValidateZipCode(zipCode);
}
public string City
{
get { return _city; }
}
public string Province
{
get { return _province; }
}
public int ZipCode
{
get { return _zipCode; }
}
}

假如要轉變Address,不克不及修正現有的實例,只能創立一個新的實例:
Address address = new Address("Chengdu", "Sichuan", 610000);//create a instance
address = new Address("Nanjing", "Jiangsu", 210000);//modify the instance

address將不存在任何有效的暫時狀況。那些暫時狀況只存在於Address的結構函數履行進程中。如許一來,Address是異常平安的,也是線程平安的。

5.3 確保0為值類型的有用狀況

.NET的默許初始化機制會將援用類型設置為二進制意義上的0,即null。而關於值類型,豈論我們能否供給結構函數,都邑有一個默許的結構函數,將其設置為0。

一種典范的情形是列舉:

public enum Sex
{
Male = 1;
Female = 2;
}

然後用做值類型的成員:

public struct Employee
{
private Sex _sex;
//other
}

創立Employee構造體將獲得一個有效的Sex字段:
Employee employee = new Employee ();

employee的_sex是有效的,由於其為0。我們應當將0作為一個為初始化的值明白表現出來:

public Sex
{
None = 0;
Male = 1;
Female = 2;
}

假如值類型中包括援用類型,會湧現另外一種初始化成績:

public struct ErrorLog
{
private string _message;
//other
}

然後創立一個ErrorLog:
ErrorLog errorLog = new ErrorLog ();

errorLog的_message字段將是一個空援用。我們應當經由過程一個屬性來將_message裸露給客戶代碼,從而使該成績限制在ErrorLog 的外部:

public struct ErrorLog
{
private string _message;
public string Message
{
get
{
return (_message ! = null) ? _message : string.Empty;
}
set { _message = value; }
}
//other
}

5.4 盡可能削減裝箱和拆箱

裝箱指把一個值類型放入一個未簽字類型的援用類型中,好比:
int valueType = 0;
object referenceType = i;//boxing

拆箱則是早年面的裝箱對象中掏出值類型:
object referenceType;
int valueType = (int)referenceType;//unboxing

裝箱和拆箱是比擬消耗機能的,還會引入一些詭異的bug,我們應該防止裝箱和拆箱。

裝箱和拆箱最年夜的成績是會主動產生。好比:
Console.WriteLine("A few numbers: {0}, {1}.", 25, 32);

個中,Console.WriteLine()吸收的參數類型是(string,object,object)。是以,現實上會履行以下操作:
int i = 25;
obeject o = i;//boxing

然後把o傳給WriteLine()辦法。在WriteLine()辦法的外部,為了挪用i上的ToString()辦法,又會履行:
int i = (int)o;//unboxing
string output = i,ToString();

所以准確的做法應當是:
Console.WriteLine("A few numbers: {0}, {1}.", 25.ToString(), 32.ToString());

25.ToString()只是履行一個辦法並前往一個援用類型,不存在裝箱/拆箱的成績。

另外一個典范的例子是ArryList的應用:

public struct Employee
{
private string _name;
public Employee(string name)
{
_name = name;
}
public string Name
{
get { return _name; }
set { _name = value; }
}
public override string ToString()
{
return _name;
}
}
ArrayList employees = new ArrayList();
employees.Add(new Employee("Old Name"));//boxing
Employee ceo = (Employee)employees[0];//unboxing
ceo.Name = "New Name";//employees[0].ToString() is still "Old Name"

下面的代碼不只存在機能的成績,還輕易招致毛病產生。

在這類情形下,更好的做法是應用泛型聚集:
List<Employee> employees = new List<Employee>();


因為List<T>是強類型的聚集,employees.Add()辦法不停止類型轉換,所以不存在裝箱/拆箱的成績。

6. 總結

C#中,變量是值照樣援用僅取決於其數據類型。

C#的值類型包含:構造體(數值類型,bool型,用戶界說的構造體),列舉,可空類型。

C#的援用類型包含:數組,用戶界說的類、接口、拜托,object,字符串。

數組的元素,不論是援用類型照樣值類型,都存儲在托管堆上。

援用類型在棧中存儲一個援用,其現實的存儲地位位於托管堆。為了便利,本文簡稱援用類型安排在托管推上。

值類型老是分派在它聲明的處所:作為字段時,追隨其所屬的變量(實例)存儲;作為部分變量時,存儲在棧上。

值類型在內存治理方面具有更好的效力,而且不支撐多態,合適用作存儲數據的載體;援用類型支撐多態,合適用於界說運用法式的行動。

應當盡量地將值類型完成為具有常量性和原子性的類型。

應當盡量地確保0為值類型的有用狀況。

應當盡量地削減裝箱和拆箱。

7. 參考
Effective C#
Professional C#
Programming .NET Components
C#說話標准
Type Fundamentals

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved