C# 語言的類型划分為兩大類:值類型 (Value type) 和引用類型 (reference type)。值類型和引用類型都可以為泛型類型 (generic type),泛型類型采用一個或多個類型參數。類型參數可以指定值類型和引用類型。
type:
value-type
reference-type
type-parameter
第三種類型是指針,只能用在不安全代碼中。第 18.2 節對此做了進一步的探討。
值類型與引用類型的不同之處在於:值類型的變量直接包含其數據,而引用類型的變量存儲對其數據的引用 (reference),后者稱為對象 (object)。對於引用類型,兩個變量可能引用同一個對象,因此對一個變量的操作可能影響另一個變量所引用的對象。對於值類型,每個變量都有自己的數據副本,對一個變量的操作不可能影響另一個變量。
C# 的類型系統是統一的,因此任何類型的值都可以按對象處理。C# 中的每個類型直接或間接地從 object 類類型派生,而 object 是所有類型的最終基類。引用類型的值都被視為 object 類型,被簡單地當作對象來處理。值類型的值則通過對其執行裝箱和拆箱操作(第 4.3 節)按對象處理。
1.1 值類型
一個值類型或是結構類型,或是枚舉類型。C# 提供稱為簡單類型 (simple type) 的預定義結構類型集。簡單類型通過保留字標識。
value-type:
struct-type
enum-type
struct-type:
type-name
simple-type
nullable-type
simple-type:
numeric-type
bool
numeric-type:
integral-type
floating-point-type
decimal
integral-type:
sbyte
byte
short
ushort
int
uint
long
ulong
char
floating-point-type:
float
double
nullable-type:
non-nullable-value-type ?
non-nullable-value-type:
type
enum-type:
type-name
與引用類型的變量不同的是,僅當該值類型是可以為 null 的類型時,值類型的變量才可包含 null 值。 對於每個不可以為 null 的值類型,都存在一個對應的可以為 null 的值類型,該類型表示相同的值集加上 null 值。
對值類型變量賦值時,會創建所賦的值的一個副本。這不同於引用類型的變量賦值,引用類型的變量賦值復制的是引用而不是由引用標識的對象。
1.1.1 System.ValueType 類型
所有值類型從類 System.ValueType 隱式繼承,后者又從類 object 繼承。任何類型都不可能從值類型派生,因此,所有值類型都是隱式密封的(第 10.1.1.2 節)。
注意,System.ValueType 本身不是 value-type, 而是 class-type,所有 value-type 都從它自動派生。
1.1.2 默認構造函數
所有值類型都隱式聲明一個稱為默認構造函數 (default constructor) 的公共無參數實例構造函數。默認構造函數返回一個零初始化實例,它就是該值類型的默認值 (default value):
- 對於所有 simple-types,默認值是由所有位都置零的位模式產生的值:
- 對於 sbyte、byte、byte、ushort、int、uint、long 和 ulong,默認值為 0。
- 對於 char,默認值為 '\x0000'。
- 對於 float,默認值為 0.0f。
- 對於 double,默認值為 0.0d。
- 對於 decimal,默認值為 0.0m。
- 對於 bool,默認值為 false。
- 對於 enum-type E,默認值為 0,該值被轉換為類型 E。
- 對於 struct-type,默認值是通過將所有值類型字段設置為它們的默認值並將所有引用類型字段設置為 null 而產生的值。
- 對於 nullable-type,默認值是一個其 HasValue 屬性為 false 且 Value 屬性未定義的實例。默認值也稱為可以為 null 的類型的 null 值 (null value)。
- 與任何其他實例構造函數一樣,值類型的默認構造函數也是用 new 運算符調用的。出於效率原因,實際上,不必故意調用它的構造函數。在下面的示例中,變量 i 和 j 都被初始化為零。
class A
{
void F() {
int i = 0;
int j = new int();
}
}
由於每個值類型都隱式地具有一個公共無形參實例構造函數,因此,一個結構類型中不可能包含一個關於無形參構造函數的顯式聲明。但允許結構類型聲明參數化實例構造函數(第 11.3.8 節)。
1.1.3 結構類型
結構類型是一種值類型,它可以聲明常量、字段、方法、屬性、索引器、運算符、實例構造函數、靜態構造函數和嵌套類型。結構類型的聲明在第 11.1 節中說明。
1.1.4 簡單類型
C# 提供稱為簡單類型 (simple type) 的預定義結構類型集。簡單類型通過保留字標識,而這些保留字只是 System 命名空間中預定義結構類型的別名,詳見下表。
.. |
化名的類型 |
sbyte |
System.SByte |
byte |
System.Byte |
short |
System.Int16 |
ushort |
System.UInt16 |
int |
System.Int32 |
uint |
System.UInt32 |
long |
System.Int64 |
ulong |
System.UInt64 |
char |
System.Char |
float |
System.Single |
double |
System.Double |
bool |
System.Boolean |
decimal |
System.Decimal |
由於簡單類型是結構類型的別名,所以每個簡單類型都具有成員。例如,int 具有在 System.Int32 中聲明的成員以及從 System.Object 繼承的成員,允許使用下面的語句:
int i = int.MaxValue; // System.Int32.MaxValue constant
string s = i.ToString(); // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method
簡單類型與其他結構類型的不同之處在於,簡單類型允許某些附加的操作:
- 大多數簡單類型允許通過編寫 literals(第 2.4.4 節)來創建值。例如,123 是類型 int 的文本,'a' 是類型 char 的文本。C# 沒有普遍地為結構類型設置類似的以文本創建值的規則,所以其他結構類型的非默認值最終總是通過這些結構類型的實例構造函數來創建的。
- 當表達式的操作數都是簡單類型常量時,編譯器可以在編譯時計算表達式。這樣的表達式稱為 constant-expression(第 7.19 節)。涉及其他結構類型所定義的運算符的表達式不被視為常量表達式。
- 通過 const 聲明可以聲明簡單類型(第 10.4 節)的常量。常量不可能屬於其他結構類型,但 static readonly 字段提供了類似的效果。
- 涉及簡單類型的轉換可以參與由其他結構類型定義的轉換運算符的計算,但用戶定義的轉換運算符永遠不能參與其他用戶定義運算符的計算(第 6.4.3 節)。
1.1.5 整型
- C# 支持 9 種整型:sbyte、byte、short、ushort、int、uint、long、ulong 和 char。整型具有以下所列的大小和取值范圍:
- sbyte 類型表示有符號 8 位整數,其值介於 -128 和 127 之間。
- byte 類型表示無符號 8 位整數,其值介於 0 和 255 之間。
- short 類型表示有符號 16 位整數,其值介於 -32768 和 32767 之間。
- ushort 類型表示無符號 16 位整數,其值介於 0 和 65535 之間。
- int 類型表示有符號 32 位整數,其值介於 -2147483648 和 2147483647 之間。
- uint 類型表示無符號 32 位整數,其值介於 0 和 4294967295 之間。
- long 類型表示有符號 64 位整數,其值介於 -9223372036854775808 和 9223372036854775807 之間。
- ulong 類型表示無符號 64 位整數,其值介於 0 和 18446744073709551615 之間。
- char 類型表示無符號 16 位整數,其值介於 0 和 65535 之間。char 類型的可能值集與 Unicode 字符集相對應。雖然 char 的表示形式與 ushort 相同,但是可以對一種類型進行的所有計算並非都可以對另一種類型執行。
整型一元運算符和二元運算符總是對有符號 32 位精度、無符號的 32 位精度、有符號 64 位精度或無符號 64 位精度進行計算:
- 對於一元運算符 + 和 ~,操作數轉換為 T 類型,其中 T 是 int、uint、long 和 ulong 中第一個可以完全表示操作數的所有可能值的類型。然后用 T 類型的精度執行運算,結果的類型是 T 類型。
- 對於一元運算符 –,操作數轉換為類型 T,其中 T 是 int 和 long 中第一個可以完全表示操作數的所有可能值的類型。然后用 T 類型的精度執行運算,結果的類型是 T 類型。一元運算符 – 不能應用於類型 ulong 的操作數。
- 對於 +、–、*、/、%、&、^、|、==、!=、>、<、>= 和 <= 二元運算符,操作數轉換為類型 T,其中 T 是 int、uint、long 和 ulong 中第一個可以完全表示兩個操作數的所有可能值的類型。然后用 T 類型的精度執行運算,運算的結果的類型也屬於 T(對於關系運算符為 bool)。對於二元運算符,不允許一個操作數為 long 類型而另一個操作數為 ulong 類型。
- 對於二元運算符 << 和 >>,左操作數轉換為 T 類型,其中 T 是 int、uint、long 和 ulong 中第一個可以完全表示操作數的所有可能值的類型。然后用 T 類型的精度執行運算,結果的類型是 T 類型。
char 類型歸類為整型類型,但它在以下兩個方面不同於其他整型:
- 不存在從其他類型到 char 類型的隱式轉換。具體而言,即使 sbyte、byte 和 ushort 類型具有完全可以用 char 類型來表示的值范圍,也不存在從 sbyte、byte 或 ushort 到 char 的隱式轉換。
- char 類型的常量必須寫成 character-literal 或帶有強制轉換為類型 char 的 integer-literal。例如,(char)10 與 '\x000A' 是相同的。
checked 和 unchecked 運算符和語句用於控制整型算術運算和轉換(第 7.6.12 節)的溢出檢查。在 checked 上下文中,溢出產生編譯時錯誤或導致引發 System.OverflowException。在 unchecked 上下文中將忽略溢出,任何與目標類型不匹配的高序位都被放棄。
1.1.6 浮點型
C# 支持兩種浮點型:float 和 double。float 和 double 類型用 32 位單精度和 64 位雙精度 IEEE 754 格式來表示,這些格式提供以下幾組值:
- 正零和負零。大多數情況下,正零和負零的行為與簡單的值零相同,但某些運算會區別對待此兩種零(第 7.8.2 節)。
- 正無窮大和負無窮大。無窮大是由非零數字被零除這樣的運算產生的。例如,1.0 / 0.0 產生正無窮大,而 –1.0 / 0.0 產生負無窮大。
- 非數字 (Not-a-Number) 值,常縮寫為 NaN。NaN 是由無效的浮點運算(如零被零除)產生的。
- 以 s × m × 2e 形式表示的非零值的有限集,其中 s 為 1 或 −1,m 和 e 由特殊的浮點類型確定:對於 float,為 0 < m < 224 並且 −149 ≤ e ≤ 104;對於 double,為 0 < m < 253 並且 −1075 ≤ e ≤ 970。非標准化的浮點數被視為有效非零值。
float 類型可表示精度為 7 位、在大約 1.5 × 10−45 到 3.4 × 1038 的范圍內的值。
double 類型可表示精度為 15 位或 16 位、在大約 5.0 × 10−324 到 1.7 × 10308 的范圍內的值。
如果二元運算符的一個操作數為浮點型,則另一個操作數必須為整型或浮點型,並且運算按下面這樣計算:
- 如果一個操作數為整型,則該操作數轉換為與另一個操作數的類型相同的浮點型。
- 然后,如果任一操作數的類型為 double,則另一個操作數轉換為 double。至少用 double 范圍和精度執行運算,結果的類型為 double(對於關系運算符則為 bool)。
- 否則,至少用 float 范圍和精度執行運算,結果的類型為 float(對於關系運算符則為 bool)。
浮點運算符(包括賦值運算符)從來不產生異常。相反,在異常情況下,浮點運算產生零、無窮大或 NaN,如下所述:
- 如果浮點運算的結果對於目標格式太小,則運算結果變成正零或負零。
- 如果浮點運算的結果對於目標格式太大,則運算結果變成正無窮大或負無窮大。
- 如果浮點運算無效,則運算的結果變成 NaN。
- 如果浮點運算的一個或兩個操作數為 NaN,則運算的結果變成 NaN。
可以用比運算的結果類型更高的精度來執行浮點運算。例如,某些硬件結構支持比 double 類型具有更大的范圍和精度的“extended”或“long double”浮點型,並隱式地使用這種更高精度類型執行所有浮點運算。只有性能開銷過大,才能使這樣的硬件結構用“較低”的精度執行浮點運算。C# 采取的是允許將更高的精度類型用於所有浮點運算,而不是強求執行規定的精度,造成同時損失性能和精度。除了傳遞更精確的結果外,這樣做很少會產生任何可察覺的效果。但是,在 x * y / z 形式的表達式中,如果其中的乘法會產生超出 double 范圍的結果,而后面的除法使臨時結果返回到 double 范圍內,則以更大范圍的格式去計算該表達式,可能會產生有限值的結果(本來應是無窮大)。
1.1.7 decimal 類型
decimal 類型是 128 位的數據類型,適合用於財務計算和貨幣計算。decimal 類型可以表示具有 28 或 29 個有效數字、從 1.0 × 10−28 到大約 7.9 × 1028 范圍內的值。
decimal 類型的有限值集的形式為 (–1)s × c × 10-e,其中符號 s 是 0 或 1,系數 c 由 0 ≤ c < 296 給定,小數位數 e 滿足 0 ≤ e ≤ 28。decimal 類型不支持有符號的零、無窮大或 NaN。decimal 可用一個以 10 的冪表示的 96 位整數來表示。對於絕對值小於 1.0m 的 decimal,它的值最多精確到第 28 位小數。對於絕對值大於或等於 1.0m 的 decimal,它的值精確到小數點后第 28 或 29 位。與 float 和 double 數據類型相反,十進制小數數字(如 0.1)可以精確地用 decimal 表示形式來表示。在 float 和 double 表示形式中,這類數字通常變成無限小數,使這些表示形式更容易發生舍入錯誤。
如果二元運算符的一個操作數為 decimal 類型,則另一個操作數必須為整型或 decimal 類型。如果存在一個整型操作數,它將在執行運算前轉換為 decimal。
decimal 類型值的運算結果是這樣得出的:先計算一個精確結果(按每個運算符的定義保留小數位數),然后舍入以適合表示形式。結果舍入到最接近的可表示值,當結果同樣地接近於兩個可表示值時,舍入到最小有效位數位置中為偶數的值(這稱為“銀行家舍入法”)。零結果總是包含符號 0 和小數位數 0。
如果十進制算術運算產生一個絕對值小於或等於 5 × 10-29 的值,則運算結果變為零。如果 decimal 算術運算產生的值對於 decimal 格式太大,則將引發 System.OverflowException。
與浮點型相比,decimal 類型具有較高的精度,但取值范圍較小。因此,從浮點型到 decimal 的轉換可能會產生溢出異常,而從 decimal 到浮點型的轉換則可能導致精度損失。由於這些原因,在浮點型和 decimal 之間不存在隱式轉換,如果沒有顯式地標出強制轉換,就不可能在同一表達式中同時使用浮點操作數和 decimal 操作數。
1.1.8 bool 類型
bool 類型表示布爾邏輯量。bool 類型的可能值為 true 和 false。
在 bool 和其他類型之間不存在標准轉換。具體而言,bool 類型與整型截然不同,不能用 bool 值代替整數值,反之亦然。
在 C 和 C++ 語言中,零整數或浮點值或 null 指針可以轉換為布爾值 false,非零整數或浮點值或非 null 指針可以轉換為布爾值 true。在 C# 中,這種轉換是通過顯式地將整數或浮點值與零進行比較,或者顯式地將對象引用與 null 進行比較來完成的。
1.1.9 枚舉類型
枚舉類型是具有命名常量的獨特的類型。每個枚舉類型都有一個基礎類型,該基礎類型必須為 byte、sbyte、short、ushort、int、uint、long 或 ulong。枚舉類型的值集和它的基礎類型的值集相同。枚舉類型的值並不只限於那些命名常量的值。枚舉類型是通過枚舉聲明(第 14.1 節)定義的。
1.1.10 可以為 null 的類型
可以為 null 的類型可以表示其基礎類型 (underlying type) 的所有值和一個額外的 null 值。可以為 null 的類型寫作 T?,其中 T 是基礎類型。此語法是 System.Nullable<T> 的簡寫形式,這兩種形式可以互換使用。
相反,不可以為 null 的值類型 (non-nullable value type) 可以是除 System.Nullable<T> 及其簡寫形式T?(對於任何類型的 T)之外的任何值類型,加上約束為不可以為 null 的值類型的任何類型參數(即具有 struct 約束的任何類型參數)。System.Nullable<T> 類型指定 T 的值類型約束(第 10.1.5 節),這意味着可以為 null 的類型的基礎類型可以是任何不可以為 null 的值類型。可以為 null 的類型的基礎類型不能是可以為 null 的類型或引用類型。例如,int?? 和 string? 是無效類型。
可以為 null 的類型 T? 的實例有兩個公共只讀屬性:
- 類型為 bool 的 HasValue 屬性
- 類型為 T 的 Value 屬性
HasValue 為 true 的實例稱為非 null。非 null 實例包含一個已知值,可通過 Value 返回該值。
HasValue 為 false 的實例稱為 null。null 實例有一個不確定的值。嘗試讀取 null 實例的 Value 將導致引發 System.InvalidOperationException。訪問可以為 null 的實例的 Value 屬性的過程稱作解包 (unwrapping)。
除了默認構造函數之外,每個可以為 null 的類型 T? 都有一個具有類型為 T 的單個實參的公共構造函數。例如,給定一個類型為 T 的值 x,調用形如
new T?(x)
的構造函數將創建 T? 的非 null 實例,其 Value 屬性為 x。為一個給定值創建可以為 null 的類型的非 null 實例的過程稱作包裝 (wrapping)。
從 null 文本轉換為 T?(第 6.1.5 節)以及從 T 轉換為 T?(第 6.1.4 節)可使用隱式轉換。
1.2 引用類型
引用類型是類類型、接口類型、數組類型或委托類型。
reference-type:
class-type
interface-type
array-type
delegate-type
class-type:
type-name
object
dynamic
string
interface-type:
type-name
array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[ dim-separatorsopt ]
dim-separators:
,
dim-separators ,
delegate-type:
type-name
引用類型值是對該類型的某個實例 (instance) 的一個引用,后者稱為對象 (object)。null 值比較特別,它兼容於所有引用類型,用來表示“沒有被引用的實例”。
1.2.1 類類型
類類型定義包含數據成員、函數成員和嵌套類型的數據結構,其中數據成員包括常量和字段,函數成員包括方法、屬性、事件、索引器、運算符、實例構造函數、析構函數和靜態構造函數。類類型支持繼承,繼承是派生類可用來擴展和專門化基類的一種機制。類類型的實例是用 object-creation-expressions(第 7.6.10.1 節)創建的。
有關類類型的介紹詳見第 10 章。
某些預定義類類型在 C# 語言中有特殊含義,如下表所示。
類類型 |
說明 |
System.Object |
所有其他類型的最終基類。請參見第 4.2.2 節。 |
System.String |
C# 語言的字符串類型。請參見第 4.2.4 節。 |
System.ValueType |
所有值類型的基類。請參見第 4.1.1 節。 |
System.Enum |
所有枚舉類型的基類。請參見第 14 章。 |
System.Array |
所有數組類型的基類。請參見第 12 章。 |
System.Delegate |
所有委托類型的基類。請參見第 15 章。 |
System.Exception |
所有異常類型的基類。請參見第 16 章。 |
1.2.2 對象類型
object 類類型是所有其他類型的最終基類。C# 中的每種類型都是直接或間接從 object 類類型派生的。
關鍵字 object 只是預定義類 System.Object 的別名。
1.2.3 dynamic 類型
dynamic 類型與 object 一樣,可以引用任何對象。在將運算符應用於 dynamic 類型的表達式時,其解析會推遲到程序運行時進行。因此,如果運算符不能合法地應用於引用的對象,在編譯過程中不會報告任何錯誤。而是在運行時解析運算符失敗時,會引發異常。
在第 4.7 節中進一步介紹了動態類型,在第 7.2.2 節中進一步介紹了動態綁定。
1.2.4 string 類型
string 類型是直接從 object 繼承的密封類類型。string 類的實例表示 Unicode 字符串。
string 類型的值可以寫為字符串(第 2.4.4.5 節)。
關鍵字 string 只是預定義類 System.String 的別名。
1.2.5 接口類型
一個接口定義一個協定。實現某接口的類或結構必須遵守該接口定義的協定。一個接口可以從多個基接口繼承,而一個類或結構可以實現多個接口。
有關接口類型的介紹詳見第 13 章。
1.2.6 數組類型
數組是一種數據結構,它包含可通過計算索引訪問的零個或更多個變量。數組中包含的變量(又稱數組的元素)具有相同的類型,該類型稱為數組的元素類型。
有關數組類型的介紹詳見第 12 章。
1.2.7 委托類型
委托是引用一個或多個方法的數據結構。對於實例方法,委托還可引用實例方法對應的對象實例。
在 C 或 C++ 中與委托最接近的是函數指針,但函數指針只能引用靜態函數,而委托則既可以引用靜態方法,也可以引用實例方法。在后一種情況中,委托不僅存儲了一個對該方法入口點的引用,還存儲了一個對相應的對象實例的引用,該方法就是通過此對象實例被調用的。
有關委托類型的介紹詳見第 15 章。
1.3 裝箱和拆箱
裝箱和拆箱的概念是 C# 的類型系統的核心。它在 value-types 和 reference-types 之間架起了一座橋梁,使得任何 value-type 的值都可以轉換為 object 類型的值,反過來轉換也可以。裝箱和拆箱使我們能夠統一地來考察類型系統,其中任何類型的值最終都可以按對象處理。
1.3.1 裝箱轉換
裝箱轉換允許將 value-type 隱式轉換為 reference-type。存在下列裝箱轉換:
- 從任何 value-type 到 object 類型。
- 從任何 value-type 到 System.ValueType 類型。
- 從任何 non-nullable-value-type 到 value-type 實現的任何 interface-type。
- 從任何 nullable-type 到由 nullable-type 的基礎類型實現的任何 interface-type。
- 從任何 enum-type 到 System.Enum 類型。
- 從任何具有基礎 enum-type 的 nullable-type 到 System.Enum 類型。
請注意,對類型形參進行隱式轉換將以裝箱轉換的形式執行(如果在運行時它最后從值類型轉換到引用類型(第 6.1.10 節))。
將 non-nullable-value-type 的一個值裝箱包括以下操作:分配一個對象實例,然后將 non-nullable-value-type 的值復制到該實例中。
對 nullable-type 的值裝箱時,如果該值為 null 值(HasValue 為 false),將產生一個 null 引用;否則將產生對基礎值解包和裝箱的結果。
最能說明 non-nullable-value-type 的值的實際裝箱過程的辦法是,設想有一個泛型裝箱類 (boxing class),其行為與下面聲明的類相似:
sealed class Box<T>: System.ValueType
{
T value;
public Box(T t) {
value = t;
}
}
T 類型值 v 的裝箱過程現在包括執行表達式 new Box<T>(v) 和將結果實例作為 object 類型的值返回。因此,下面的語句
int i = 123;
object box = i;
在概念上相當於
int i = 123;
object box = new Box<int>(i);
實際上,像上面這樣的 Box<T>裝箱類並不存在,並且裝箱值的動態類型也不會真的屬於一個類類型。相反,T 類型的裝箱值屬於動態類型 T,若用 is 運算符來檢查動態類型,也僅能引用類型 T。例如,
int i = 123;
object box = i;
if (box is int) {
Console.Write("Box contains an int");
}
將在控制台上輸出字符串“Box contains an int”。
裝箱轉換隱含着復制一份 待裝箱的值。這不同於從 reference-type 到 object 類型的轉換,在后一種轉換中,轉換后的值繼續引用同一實例,只是將它當作派生程度較小的 object 類型而已。例如,給定下面的聲明
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
則下面的語句
Point p = new Point(10, 10);
object box = p;
p.x = 20;
Console.Write(((Point)box).x);
將在控制台上輸出值 10,因為將 p 賦值給 box 是一個隱式裝箱操作,它將復制 p 的值。如果將 Point 聲明為 class,由於 p 和 box 將引用同一個實例,因此輸出值為 20。
1.3.2 拆箱轉換
取消裝箱轉換允許將 reference-type 顯式轉換為 value-type。存在以下拆箱轉換:
- 從 object 類型到任何 value-type。
- 從 System.ValueType 類型到任何 value-type。
- 從任何 interface-type 到實現了該 interface-type 的任何 non-nullable-value-type。
- 從任何 interface-type 到其基礎類型實現了該 interface-type 的任何 nullable-type。
- 從 System.Enum 類型到任何 enum-type。
- 從 System.Enum 類型到任何具有基礎 enum-type 的 nullable-type。
請注意,到類型形參的顯式轉換將以取消裝箱轉換的形式執行(如果在運行時它結束從引用類型到值類型(第 6.2.6 節)的轉換)。
對 non-nullable-value-type 取消裝箱的操作包括下列步驟:首先檢查對象實例是否是給定 non-nullable-value-type 的裝箱值,然后將該值從實例中復制出來。
對 nullable-type 取消裝箱在源操作數為 null 時會產生 nullable-type 的 null 值;否則將產生從對象實例到 nullable-type 的基礎類型的取消裝箱的包裝結果。
參照前一節中關於假想的裝箱類的描述,從對象 box 到 value-type T 的取消裝箱轉換包括執行表達式 ((Box<T>)box).value。因此,下面的語句
object box = 123;
int i = (int)box;
在概念上相當於
object box = new Box<int>(123);
int i = ((Box<int>)box).value;
為使針對給定 non-nullable-value-type 的取消裝箱轉換在運行時取得成功,源操作數的值必須是對該 non-nullable-value-type 的裝箱值的引用。如果源操作數為 null,則將引發 System.NullReferenceException。如果源操作數是對不兼容對象的引用,則將引發 System.InvalidCastException。
為使針對給定 nullable-type 的取消裝箱轉換在運行時取得成功,源操作數的值必須是 null 或是對該 nullable-type 的基礎 non-nullable-value-type 的裝箱值的引用。如果源操作數是對不兼容對象的引用,則將引發 System.InvalidCastException。
1.4 構造類型
泛型類型聲明本身表示未綁定的泛型類型 (unbound generic type),它通過應用類型實參 (type argument) 被用作構成許多不同類型的“藍圖”。類型實參編寫在緊跟在泛型類型的名稱后面的尖括號(< 和 >)中。至少包括一個類型實參的類型稱為構造類型 (constructed type)。構造類型可以在語言中能夠出現類型名的大多數地方使用。未綁定的泛型類型只能在 typeof-expression(第 7.6.11 節)中使用。
構造類型還可以在表達式中用作簡單名稱(第 7.6.2 節)或在訪問成員時使用(第 7.6.4 節)。
在計算 namespace-or-type-name 時,僅考慮具有正確數目的類型形參的泛型類型。因此,可以使用同一個標識符標識不同的類型,前提是那些類型具有不同數目的類型形參。當在同一程序中混合使用泛型和非泛型類時,這是很有用的:
namespace Widgets
{
class Queue {...}
class Queue<TElement> {...}
}
namespace MyApplication
{
using Widgets;
class X
{
Queue q1; // Non-generic Widgets.Queue
Queue<int> q2; // Generic Widgets.Queue
}
}
即使未直接指定類型形參,type-name 也可以標識構造類型。當某個類型嵌套在泛型類聲明中,並且包含該類型的聲明的實例類型被隱式用於名稱查找(第 10.3.8.6 節)時,就會出現這種情況:
class Outer<T>
{
public class Inner {...}
public Inner i; // Type of i is Outer<T>.Inner
}
在不安全代碼中,構造類型不能用作 unmanaged-type(第 18.2 節)。
1.4.1 類型實參
類型實參列表中的每個實參都只是一個 type。
type-argument-list:
< type-arguments >
type-arguments:
type-argument
type-arguments , type-argument
type-argument:
type
在不安全代碼(第 18 章)中,type-argument 不可以是指針類型。每個類型實參都必須滿足對應的類型形參上的所有約束(第 10.1.5 節)。
1.4.2 開放和封閉類型
所有類型都可歸類為開放類型 (open type) 或封閉類型 (closed type)。開放類型是包含類型形參的類型。更明確地說:
- 類型形參定義開放類型。
- 當且僅當數組元素類型是開放類型時,該數組類型才是開放類型。
- 當且僅當構造類型的一個或多個類型實參為開放類型時,該構造類型才是開放類型。當且僅當構造的嵌套類型的一個或多個類型實參或其包含類型的類型實參為開放類型時,該構造的嵌套類型才是開放類型。
封閉類型是不屬於開放類型的類型。
在運行時,泛型類型聲明中的所有代碼都在一個封閉構造類型的上下文中執行,這個封閉構造類型是通過將類型實參應用該泛型聲明來創建的。泛型類型中的每個類型形參都綁定到特定的運行時類型。所有語句和表達式的運行時處理都始終使用封閉類型,開放類型僅出現在編譯時處理過程中。
每個封閉構造類型都有自己的靜態變量集,任何其他封閉構造類型都不會共享這些變量。由於開放類型在運行時並不存在,因此不存在與開放類型關聯的靜態變量。如果兩個封閉構造類型是從相同的未綁定泛型類型構造的,並且它們的對應類型實參屬於相同類型,則這兩個封閉構造類型是相同類型。
1.4.3 綁定和未綁定類型
術語未綁定類型 (unbound type) 是指非泛型類型或未綁定的泛型類型。術語綁定類型 (bound type) 是指非泛型類型或構造類型。
未綁定類型是指類型聲明所聲明的實體。未綁定泛型類型本身不是一種類型,不能用作變量、參數或返回值的類型,也不能用作基類型。可以引用未綁定泛型類型的唯一構造是 typeof 表達式(第 7.6.11 節)。
1.4.4 滿足約束
每當引用構造類型或泛型方法時,都會根據泛型類型或方法(第 10.1.5 節)上聲明的類型形參約束對所提供的類型實參進行檢查。對於每個 where 子句,將根據每個約束檢查與命名的類型形參相對應的類型實參 A,如下所示:
- 如果約束為類類型、接口類型或類型形參,則假設 C 表示該約束,並用所提供的類型實參替換出現在該約束中的任何類型形參。若要滿足該約束,必須可通過下列方式之一將類型 A 轉換為類型 C:
- 標識轉換(第 6.1.1 節)
- 隱式引用轉換(第 6.1.6 節)
- 裝箱轉換(第 6.1.7 節)— 前提是類型 A 為不可以為 null 的值類型。
- 從類型形參 A 到 C 的隱式引用、裝箱或類型形參轉換。
- 如果約束為引用類型約束 (class) 則類型 A 必須滿足下列條件之一:
- A 為接口類型、類類型、委托類型或數組類型。注意,System.ValueType 和 System.Enum 是滿足此約束的引用類型。
- A 是已知為引用類型的類型形參(第 10.1.5 節)。
- 如果約束為值類型約束 (struct) 則類型 A 必須滿足下列條件之一:
- A 為結構類型或枚舉類型,但不是可以為 null 的類型。請注意,System.ValueType 和 System.Enum 是不滿足此約束的引用類型。
- A 為具有值類型約束的類型形參(第 10.1.5 節)。
- 如果約束為構造函數約束 new(),則類型 A 一定不能為 abstract,並且必須具有公共無形參構造函數。如果下列條件之一成立,則滿足此條件:
- A 為值類型,因為所有值類型都具有公共默認構造函數(第 4.1.2 節)。
- A 為具有構造函數約束的類型形參(第 10.1.5 節)。
- A 為具有值類型約束的類型形參(第 10.1.5 節)。
- A 是不為 abstract 並且包含顯式聲明的無參數 public 構造函數的類。
- A 不為 abstract,並且具有默認構造函數(第 10.11.4 節)。
如果給定的類型實參未滿足一個或多個類型形參的約束,則會發生編譯時錯誤。
由於類型形參未被繼承,因此約束也從不被繼承。在下面的示例中,T 需要指定其類型形參 T 上的約束,以便 T 滿足基類 B<T> 所施加的約束。相反,類 E 不需要指定約束,因為對於任何 T,List<T> 都實現 IEnumerable。
class B<T> where T: IEnumerable {...}
class D<T>: B<T> where T: IEnumerable {...}
class E<T>: B<List<T>> {...}
1.5 類型形參
類型形參是指定形參在運行時要綁定到的值類型或引用類型的標識符。
type-parameter:
identifier
由於類型形參可使用許多不同的實際類型實參進行實例化,因此類型形參具有與其他類型稍微不同的操作和限制。這包括:
- 不能直接使用類型形參聲明基類(第 10.2.4 節)或接口(第 13.1.3 節)。
- 類型形參上的成員查找規則取決於應用到該類型形參的約束(如果有)。這將在第 7.4 節中詳細描述。
- 類型形參的可用轉換取決於應用到該類型形參的約束(如果有)。這將在第 6.1.10 和 6.2.6 節中詳細描述。
- 如果事先不知道由類型形參給出的類型是引用類型(第 6.1.10 節),不能將標識 null 轉換為該類型。不過,可以改為使用 default 表達式(第 7.6.13 節)。此外,具有由類型形參給出的類型的值可以 使用 == 和 != 與 null 進行比較(第 7.10.6 節),除非該類型形參具有值類型約束。
- 僅當類型形參受 constructor-constraint 或值類型約束(第 7.6.10.1 節)的約束時,才能將 new 表達式(第 10.1.5 節)與類型形參聯合使用。
- 不能在特性中的任何位置上使用類型形參。
- 不能在成員訪問(第 7.6.4 節)或類型名稱(第 3.8 節)中使用類型形參標識靜態成員或嵌套類型。
- 在不安全代碼中,類型形參不能用作 unmanaged-type(第 18.2 節)。
作為類型,類型形參純粹是一個編譯時構造。在運行時,每個類型形參都綁定到一個運行時類型,運行時類型是通過向泛型類型聲明提供類型實參來指定的。因此,使用類型形參聲明的變量的類型在運行時將是封閉構造類型(第 4.4.2 節)。涉及類型形參的所有語句和表達式的運行時執行都使用作為該形參的類型實參提供的實際類型。
1.6 表達式樹類型
表達式樹 (Expression tree) 允許匿名函數表示為數據結構而不是可執行代碼。表達式樹是 System.Linq.Expressions.Expression<D> 形式的表達式樹類型 (expression tree type) 的值,其中 D 是任何委托類型。對於本規范的其余部分,我們將使用簡寫形式 Expression<D> 引用這些類型。
如果存在從匿名函數到委托類型 D 的轉換,則也存在到表達式樹類型 Expression<D> 的轉換。不過,匿名函數到委托類型的轉換會生成一個引用該匿名函數的可執行代碼的委托,而到表達式樹類型的轉換則會創建該匿名函數的表達式樹表示形式。
表達式樹是匿名函數有效的內存數據表示形式,它使匿名函數的結構變得透明和明晰。
與委托類型 D 一樣,Expression<D> 具有與 D 相同的參數和返回類型。
下面的示例將匿名函數表示為可執行代碼和表達式樹。因為存在到 Func<int,int> 的轉換,所以也存在到 Expression<Func<int,int>> 的轉換:
Func<int,int> del = x => x + 1; // Code
Expression<Func<int,int>> exp = x => x + 1; // Data
進行上面的賦值之后,委托 del 引用返回 x + 1 的方法,表達式目錄樹 exp 引用描述表達式 x => x + 1 的數據結構。
泛型類型 Expression<D> 的准確定義以及當將匿名函數轉換為表達式樹類型時用於構造表達式樹的確切規則不在本規范的范圍之內,將另作說明。
有兩個要點需要明確指出:
- 並非所有匿名函數都能表示為表達式樹。例如,具有語句體的匿名函數和包含賦值表達式的匿名函數就不能表示為表達式樹。在這些情況下,轉換仍存在,但在編譯時將失敗。
- Expression<D> 提供一個實例方法 Compile,該方法產生一個類型為 D 的委托:
Func<int,int> del2 = exp.Compile();
調用此委托將導致執行表達式樹所表示的代碼。因此,根據上面的定義,del 和 del2 等效,而且下面的兩個語句也將等效:
int i1 = del(1);
int i2 = del2(1);
執行此代碼后,i1 和 i2 的值都為 2。
1.7 dynamic 類型
dynamic 類型在 C# 中具有特殊含義。其用途在於允許進行動態綁定(在第 7.2.2 節中進行了詳細介紹)。
dynamic 被視為與 object 相同,除了以下這些方面:
- 對 dynamic 類型的表達式進行的運算可以動態綁定(第 7.2.2 節)。
- 類型推斷(第 7.5.2 節)在 dynamic 和 object 都是候選項時,會優先考慮前者。
由於此等效性,因此存在以下情況:
- object 與 dynamic 之間,以及對於在將 dynamic 替換為 object 時相同的構造類型之間,存在隱式標識轉換
- 與 object 之間的隱式和顯式轉換也適用於 dynamic。
- 在將 dynamic 替換為 object 時相同的方法簽名視為是相同的簽名
dynamic 類型在運行時與 object 沒有區別。
dynamic 類型的表達式稱為動態表達式 (dynamic expression)。