共用方式為


8 種類型

8.1 一般

C# 語言的類型分為兩個主要類別:參考類型和實值型別。 實值型別和參考型別可以是泛型型別,採用一或多個型別參數 類型參數可以同時指定實值型別和參考型別。

type
    : reference_type
    | value_type
    | type_parameter
    | pointer_type     // unsafe code support
    ;

pointer_type (•23.3) 僅適用於不安全的程式代碼 (~23)。

實值型別與參考型別不同,因為實值型別的變數會直接包含其數據,而參考型別的變數則儲存其數據的參考,後者稱為 物件。 使用參考型別時,兩個變數可以參考相同的物件,因此一個變數上的作業可能會影響另一個變數所參考的物件。 使用實值型別時,變數各有自己的數據複本,而且無法讓一個上的作業影響另一個。

注意:當變數是參考或輸出參數時,它沒有自己的記憶體,而是參考另一個變數的記憶體。 在此情況下,ref 或 out 變數實際上是另一個變數的別名,而不是相異變數。 end note

C# 的類型系統是統一的, 因此任何類型的值都可以視為物件。 C# 中的每個型別都直接或間接衍生自 object 類別型別,而 object 是所有型別的基底類別。 參考型別的值之所以會視為物件,只是將這些值當作 object 型別來檢視。 實值型別的值會藉由執行 Boxing 和 unboxing 作業來視為物件(~8.3.13)。

為了方便起見,在此規格中,會撰寫某些連結庫類型名稱,而不需使用其完整名稱資格。 如需詳細資訊, 請參閱 >C.5

8.2 參考類型

8.2.1 一般

引用類型是類別類型、介面類型、數位型別、委派類型或 dynamic 類型。 對於每個不可為 Null 的參考型別,會將 附加 ? 至類型名稱,以指出對應的可為 Null 參考型別。

reference_type
    : non_nullable_reference_type
    | nullable_reference_type
    ;

non_nullable_reference_type
    : class_type
    | interface_type
    | array_type
    | delegate_type
    | 'dynamic'
    ;

class_type
    : type_name
    | 'object'
    | 'string'
    ;

interface_type
    : type_name
    ;

array_type
    : non_array_type rank_specifier+
    ;

non_array_type
    : value_type
    | class_type
    | interface_type
    | delegate_type
    | 'dynamic'
    | type_parameter
    | pointer_type      // unsafe code support
    ;

rank_specifier
    : '[' ','* ']'
    ;

delegate_type
    : type_name
    ;

nullable_reference_type
    : non_nullable_reference_type nullable_type_annotation
    ;

nullable_type_annotation
    : '?'
    ;

pointer_type僅適用於不安全的程序代碼(~23.3)。 nullable_reference_type會在 \8.9進一步討論。

參考型別值是型別實例的參考,後者稱為 物件。 特殊值 null 與所有參考型別相容,並指出實例不存在。

8.2.2 類別類型

類別類型定義數據結構,其中包含數據成員(常數和欄位)、函式成員(方法、屬性、事件、索引器、運算元、實例建構函式、完成項和靜態建構函式),以及巢狀類型。 類別類型支持繼承,這是衍生類別可以擴充和特製化基類的機制。 類別類型的實例是使用 sobject_creation_expression 建立的(~12.8.17.2)。

類別類型會在 •15描述。

某些預先定義的類別類型在 C# 語言中具有特殊意義,如下表所述。

類別類型 說明
System.Object 所有其他類型的最終基類。 請參閱 <8.2.3
System.String C# 語言的字串類型。 請參閱 <8.2.5
System.ValueType 所有實值型別的基類。 請參閱 •8.3.2
System.Enum 所有 enum 型別的基類。 請參閱 >19.5
System.Array 所有數位類型的基類。 請參閱 <17.2.2
System.Delegate 所有 delegate 型別的基類。 請參閱 >20.1
System.Exception 所有例外狀況類型的基類。 請參閱 >21.3

8.2.3 物件類型

類別 object 類型是所有其他類型的最終基類。 C# 中的每個類型直接或間接衍生自 object 類別類型。

關鍵詞 object 只是預先定義類別 System.Object的別名。

8.2.4 動態類型

dynamic型別,例如 object,可以參考任何物件。 當作業套用至 類型的 dynamic表達式時,其解析度會延後,直到執行程序為止。 因此,如果作業無法合法地套用至參考的物件,則編譯期間不會提供任何錯誤。 相反地,當作業解析在運行時間失敗時,將會擲回例外狀況。

dynamic類型會在 \8.7進一步說明,以及 12.3.1 中的動態系結。

8.2.5 字串類型

string 類型是繼承自 object的密封類別類型。 類別的 string 實例代表 Unicode 字元字串。

string型別的值可以寫入為字串常值(~6.4.5.6)。

關鍵詞 string 只是預先定義類別 System.String的別名。

8.2.6 介面類型

介面會定義合約。 實作介面的類別或結構應遵守其合約。 介面可能繼承自多個基底介面,而類別或結構可能會實作多個介面。

介面類型會在 •18描述。

8.2.7 數位型態

數位是包含零個或多個變數的數據結構,可透過計算索引來存取。 陣列中包含的變數 (也稱為陣列的元素) 屬於相同的型別,這種型別稱為陣列的元素型別。

數位型態會在 •17描述。

8.2.8 委派類型

委派是參考一或多個方法的數據結構。 針對實例方法,它也會參考其對應的物件實例。

注意:C 或 C++ 中委派最接近的對等專案是函式指標,但函式指標只能參考靜態函式,但委派可以同時參考靜態和實例方法。 在後者的情況下,委派不僅會儲存方法進入點的參考,也會儲存要叫用方法的對象實例參考。 end note

委派類型會在 •20描述。

8.3 實值型別

8.3.1 一般

實值型別為結構類型或列舉型別。 C# 提供一組稱為 簡單型別的預先定義結構類型。 簡單類型是透過關鍵詞來識別。

value_type
    : non_nullable_value_type
    | nullable_value_type
    ;

non_nullable_value_type
    : struct_type
    | enum_type
    ;

struct_type
    : type_name
    | simple_type
    | tuple_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'
    ;

tuple_type
    : '(' tuple_type_element (',' tuple_type_element)+ ')'
    ;
    
tuple_type_element
    : type identifier?
    ;
    
enum_type
    : type_name
    ;

nullable_value_type
    : non_nullable_value_type nullable_type_annotation
    ;

不同於參考型別的變數,實值型別的變數只有在實值型別是可為 Null 的實值型別時,才能包含值 null。 對於每個不可為 Null 的實值型別,都有對應的可為 Null 實值型別,表示相同的值集加上 值 null

指派給實值型別的變數會 建立所指派值的複本 。 這與參考型別的變數指派不同,該變數會複製參考,但不會複製參考所識別的物件。

8.3.2 System.ValueType 類型

所有實值型別都會隱含繼承自 class System.ValueType,接著會繼承自 類別 object。 無法讓任何類型衍生自實值型別,因此實值型別會隱含密封(\15.2.2.3)。

請注意, System.ValueType 本身不是 value_type。 相反地,這是class_type,所有value_type都會自動衍生。

8.3.3 預設建構函式

所有實值型別都會隱含宣告稱為預設建構函式的 公用無參數實例建構函式。 預設建構函式會傳回以零初始化的實例,稱為 實值類型的預設值

  • 針對所有 simple_types,預設值是所有零的位模式所產生的值:
    • 針對sbytebyteshort、、ushort、、longuintint、 和 ulong,預設值為 0
    • 針對 char預設值為 '\x0000'
    • 針對 float預設值為 0.0f
    • 針對 double預設值為 0.0d
    • 針對 decimal,預設值為 0m (也就是小數字數為0的值零)。
    • 針對 bool預設值為 false
    • 針對 enum_type E,預設值為 0,會轉換成 類型E
  • 對於struct_type,預設值是將所有實值類型欄位設定為預設值所產生的值,並將所有參考類型欄位設定為 null
  • 對於nullable_value_type預設值是 屬性為 false 的HasValue實例。 預設值也稱為 可為 Null 實值類型的 Null 值 。 嘗試讀取 Value 這類值的 屬性會導致擲回型 System.InvalidOperationException 別的例外狀況(\8.3.12)。

如同任何其他實例建構函式,會使用 運算子叫用 new 實值型別的預設建構函式。

注意:基於效率考慮,這項需求並不是要實際讓實作產生建構函式呼叫。 針對實值型別,預設值表達式 (~12.8.21) 會產生與使用預設建構函式相同的結果。 end note

範例:在下列程式代碼中,變數 ik j 都會初始化為零。

class A
{
    void F()
    {
        int i = 0;
        int j = new int();
        int k = default(int);
    }
}

end 範例

因為每個實值型別都隱含具有公用無參數實例建構函式,所以結構類型不可能包含無參數建構函式的明確宣告。 不過,結構類型允許宣告參數化實例建構函式(~16.4.9)。

8.3.4 結構類型

結構類型是實值類型,可以宣告常數、欄位、方法、屬性、事件、索引器、運算符、實例建構函式、靜態建構函式和巢狀類型。 結構類型的宣告會在 \16描述。

8.3.5 簡單類型

C# 提供一組稱為簡單型別的預先 struct 定義型別。 簡單類型是透過關鍵詞來識別,但這些關鍵詞只是命名空間中System預先定義struct類型的別名,如下表所述。

關鍵字 別名類型
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

end 範例

注意:簡單類型與其他結構類型不同,因為它們允許某些額外的作業:

  • 大部分的簡單型別都允許藉由撰寫 常值 來建立值(~6.4.5),不過 C# 一般不會布建結構類型的常值。 範例123 是 類型的 int 常值,而 'a' 是 類型的 char常值。 end 範例
  • 當表達式的操作數都是簡單的類型常數時,編譯程式可以在編譯時期評估表達式。 這類表達式稱為 constant_expression~12.23)。 涉及其他結構類型所定義之運算子的表達式不會被視為常數表達式
  • 透過 const 宣告,可以宣告簡單類型的常數({15.4)。 不可能有其他結構類型的常數,但靜態只讀字段會提供類似的效果。
  • 涉及簡單型別的轉換可以參與其他結構類型所定義的轉換運算符評估,但使用者定義的轉換運算符永遠不能參與另一個使用者定義轉換運算符的評估(^10.5.3)。

end note.

8.3.6 整數類型

C# 支援九個整數類型:sbyte、、byteshort、、intushort、、uintlongulongchar。 整數類型具有下列值的大小和範圍:

  • sbyte 類型代表帶正負號的8位整數,其值為from -128127,內含。
  • byte 類型代表不帶正負號的8位整數,其值為from 0255,內含。
  • short 別代表帶正負號的16位整數,其值為from -3276832767,內含。
  • ushort 類型代表不帶正負號的16位整數,其值為from 065535,內含。
  • int 類型代表帶正負號的32位整數,其值為from -21474836482147483647,內含。
  • uint 類型代表不帶正負號的32位整數,其值為from 04294967295,內含。
  • long 別代表帶正負號的64位整數,其值為from -92233720368547758089223372036854775807,包含。
  • ulong 類型代表不帶正負號的 64 位整數,其值為 from 018446744073709551615,內含。
  • char 類型代表不帶正負號的16位整數,其值為from 065535,內含。 型別的 char 可能值集合會對應至 Unicode 字元集。

    注意:雖然 char 具有與 ushort相同的表示法,但另一種類型上不允許所有允許的作業。 end note

所有帶正負號的整數類型都會使用兩個的補碼格式來表示。

integral_type一元和二元運算元一律會以帶正負號的 32 位有效位數、不帶正負號的 32 位有效位數、帶正負號的 64 位有效位數或不帶正負號的 64 位有效位數運作,如12.4.7 中所述

char 類型分類為整數類型,但有兩種方式與其他整數類型不同:

  • 沒有從其他類型到型別 char 的預先定義隱含轉換。 特別是,即使 byteushort 型別具有使用 char 型別完全可表示的值範圍,但從 sbyte、byte 或 ushort 隱含轉換不存在 char
  • 型別的 char 常數應寫入為 character_literals 或 integer_literal,並結合轉換成類型 char。

範例(char)10'\x000A'相同。 end 範例

checkedunchecked 運算符和 語句可用來控制整數型別算術運算和轉換的溢位檢查(~12.8.20)。 checked在內容中,溢位會產生編譯時期錯誤,或導致 System.OverflowException 擲回 。 unchecked在內容中,會忽略溢位,並捨棄不符合目的地類型的任何高階位。

8.3.7 浮點類型

C# 支援兩種浮點類型: floatdoublefloatdouble 類型是使用 32 位單精度和 64 位雙精確度 IEC 60559 格式來表示,其提供下列值集:

  • 正零和負零。 在大部分情況下,正零和負零的行為與簡單值零相同,但某些作業會區分兩者(~12.10.3)。
  • 正無限大和負無限大。 無限是藉由將非零數字除以零之類的運算來產生。

    範例1.0 / 0.0 產生正無限大,併 –1.0 / 0.0 產生負無限大。 end 範例

  • Not-a-Number 值,通常縮寫為 NaN。 NaN 是由不正確的浮點運算所產生,例如零除以零。
  • 格式 s × m × 2e 之非零值的有限集合, 其中 s 為 1 或 \1,m e 是由特定的浮點類型所決定:針對 float、0 m 2ー⁴ 和 \149 ≤ e ≤ 104,而 針對 double,0<< m<< 2⁵ー 和 \1075 ≤ e ≤ 970。 反正規化的浮點數會被視為有效的非零值。 C# 既不需要也不禁止符合實作支援反正規化的浮點數。

float 類型可以代表介於大約 1.5 × 10⁻⁴⁵ 到 3.4 × 10ー⁸,有效位數為 7 位數的值。

double 類型可以代表介於大約5.0×10⁻ー⁴ 到1.7× 10⁰⁸,有效位數為15-16位數的值。

如果二元運算符的任一操作數是浮點類型,則會套用標準數值升階,如 ^12.4.7 中所述,且作業會以 floatdouble 有效位數執行。

浮點運算符,包括指派運算符,永遠不會產生例外狀況。 相反地,在特殊情況下,浮點運算會產生零、無限大或 NaN,如下所述:

  • 浮點運算的結果會四捨五入為目的格式中最接近的可表示值。
  • 如果浮點運算的結果大小對於目的格式而言太小,則作業的結果會變成正零或負零。
  • 如果浮點運算的結果大小太大而無法達到目的格式,則作業的結果會變成正無限大或負無限大。
  • 如果浮點運算無效,作業的結果會變成 NaN。
  • 如果浮點運算的一或兩個運算元都是 NaN,則運算的結果會變成 NaN。

浮點運算的精確度可能高於作業的結果類型。 若要強制浮點型別的值與其型別的精確精確度,可以使用明確轉換(~12.9.7)。

範例:某些硬體架構支援範圍和精確度大於 double 類型的「擴充」或「長雙精度」浮點類型,並使用這個較高的有效位數類型隱含地執行所有浮點運算。 只有在效能成本過高時,這類硬體架構才能以較低的精確度執行浮點運算,而不需要實作來取代效能和精確度,C# 可讓所有浮點運算使用更高的精確度類型。 除了提供更精確的結果,這很少會產生任何可測量的效果。 不過,在表單 x * y / z的表達式中,乘法會產生超出範圍的結果 double ,但後續的除法會將暫存結果帶回 double 範圍,因此表達式是以較高的範圍格式進行評估,可能會導致產生有限結果,而不是無限結果。 end 範例

8.3.8 十進位類型

decimal 型別是 128 位元的資料型別,適合財務和貨幣計算。 此 decimal 類型可以代表值,包括範圍中至少為 -7.9 × 10⁻ー⁸ 到 7.9 × 10⁸ 的值,且精確度至少為 28 位數。

類型的decimal有限值集是 c × c × 10⁻e, 其中符號 v 為 0 或 1,則係數 c 是由 0 ≤ c Cmax< 所指定,而刻e 是 emin eEmax,其中 Cmax 至少為 1 × 10ー⁸,Emin ≤ 0, Emax ≥ 28。 此 decimal 類型不一定支援帶正負號的零、無限或 NaN。

decimal表示為以十個乘冪縮放的整數。 對於 decimal絕對值小於 1.0m的 s,此值至少與第 28 個小數位數完全相符。 對於 decimal絕對值大於或等於 1.0m的 s,這個值會精確到至少 28 位數。 與 floatdouble 數據類型相反,例如的 0.1 十進位小數數位可以完全以十進位表示來表示。 在 floatdouble 表示法中,這類數位通常會有非終止的二進位展開,使得這些表示更容易捨入錯誤。

如果二元運算符的任一操作數為 decimal 類型,則會套用標準數值升階,如 ^12.4.7 中所述,且作業會以double有效位數執行。

decimal 別值的運算結果是計算確切的結果(保留每個運算子所定義的小數位數),然後四捨五入以符合表示法。 結果會四捨五入為最接近的可表示值,而當結果同樣接近兩個可表示值時,會捨入到具有最小有效位數位置之偶數的值(這稱為「銀行家的四捨五入」)。 也就是說,結果至少與第28個小數位數完全一樣。 請注意,四捨五入可能會從非零值產生零值。

decimal如果算術運算產生其大小太大而無法decimal格式化的結果,System.OverflowException則會擲回 。

decimal 類型的有效位數較大,但範圍可能小於浮點類型。 因此,從浮點類型 decimal 轉換成 可能會產生溢位例外狀況,而從 decimal 轉換成浮點類型可能會導致精確度或溢位例外狀況遺失。 基於這些原因,浮點類型與 decimal之間沒有隱含轉換,而且沒有明確轉換,當浮點和 decimal 操作數直接在相同的表達式中混合時,就會發生編譯時期錯誤。

8.3.9 布爾類型

bool 類型代表布爾邏輯數量。 類型的booltrue可能值為 和 false。 的表示false法會在 •8.3.3描述。 雖然的表示法未指定,但應該與的false表示true不同。

與其他實值類型之間 bool 沒有標準轉換。 特別是,此 bool 類型與整數類型不同, bool 無法使用值來取代整數值,反之亦然。

注意:在 C 和 C++ 語言中,零整數或浮點值,或 Null 指標可以轉換成布爾值 false,以及非零整數或浮點值,或非 Null 指標可以轉換成布爾值 true。 在 C# 中,這類轉換是藉由明確比較整數或浮點值到零來完成,或藉由明確比較 對 null的對象參考來完成。 end note

8.3.10 列舉類型

列舉類型是具有具名常數的相異型別。 每個列舉型別都有基礎類型,其應該是 byte、、sbyte、、shortintushort、、 uintlongulong。 列舉型別的值集與基礎型別的值集相同。 列舉型別的值不限於具名常數的值。 列舉型別是透過列舉宣告來定義 ({19.2)。

8.3.11 Tuple 類型

Tuple 類型代表具有選擇性名稱和個別型別之值的已排序固定長度序列。 Tuple 類型中的元素數目稱為其arity 元組類型是以 n ≥ 2 撰寫 (T1 I1, ..., Tn In) ,其中標識碼 I1...In 是選擇性 Tuple 元素名稱

此語法是使用 型別所System.ValueTuple<...>建構T1...Tn的類型速記,其應該是一組泛型結構類型,能夠直接表示介於 2 到 7 之間任何 Arity 的元組類型。 不需要有 System.ValueTuple<...> 直接符合任何 Tuple 類型 arity 且具有對應類型參數數目的宣告。 相反地,具有大於 7 的 Arity Tuple 會以泛型結構類型 System.ValueTuple<T1, ..., T7, TRest> 表示,除了 Tuple 元素具有 Rest 包含其餘元素巢狀值的欄位之外,使用另一種 System.ValueTuple<...> 類型。 這類巢狀可透過各種方式觀察,例如透過欄位的存在 Rest 。 其中只需要單一額外的欄位,就會使用泛型結構類型 System.ValueTuple<T1> ;此類型本身不會被視為 Tuple 類型。 如果需要超過七個額外的欄位, System.ValueTuple<T1, ..., T7, TRest> 則會以遞歸方式使用。

Tuple 類型內的項目名稱應該不同。 格式 ItemX的 Tuple 項目名稱,其中 X 是任何代表 Tuple 專案位置的非0起始小數位序列,只允許在 所 X表示的位置。

選擇性項目名稱不會在型別中 ValueTuple<...> 表示,而且不會儲存在 Tuple 值的運行時間表示法中。 身分識別轉換 (~10.2.2) 存在於具有元素類型可識別轉換序列的 Tuple 之間。

new運算子 ^12.8.17.2 無法套用 Tuple 類型語法 new (T1, ..., Tn)。 您可以從 Tuple 運算式 (^12.8.6) 建立 Tuple 值,或將運算子直接套用 new 至從 ValueTuple<...>建構的型別。

Tuple 元素是具有名稱 Item1Item2等的公用字段,而且可以透過 Tuple 值的成員存取來存取 (~12.8.7)。 此外,如果 Tuple 類型具有指定項目的名稱,則可以使用該名稱來存取有問題的元素。

注意:即使以巢狀 System.ValueTuple<...> 值來表示大型 Tuple,每個 Tuple 元素仍然可以直接使用 Item... 對應至其位置的名稱來存取。 end note

範例:假設有下列範例:

(int, string) pair1 = (1, "One");
(int, string word) pair2 = (2, "Two");
(int number, string word) pair3 = (3, "Three");
(int Item1, string Item2) pair4 = (4, "Four");
// Error: "Item" names do not match their position
(int Item2, string Item123) pair5 = (5, "Five");
(int, string) pair6 = new ValueTuple<int, string>(6, "Six");
ValueTuple<int, string> pair7 = (7, "Seven");
Console.WriteLine($"{pair2.Item1}, {pair2.Item2}, {pair2.word}");

pair2pair3pair1元組類型都是有效的,名稱為 no、部分或所有 Tuple 類型專案。

的 Tuple 類型pair4是有效的,因為名稱Item1與其Item2位置相符,而的 Tuple 類型pair5則不允許,因為名稱和Item2Item123不是。

pair7pair6宣告會示範 Tuple 型別與窗體ValueTuple<...>的建構型別可互換,而new運算符則允許使用後者的語法。

最後一行顯示,如果存在於類型中,則 Tuple 元素可以透過 Item 對應至其位置的名稱,以及對應的 Tuple 元素名稱來存取。 end 範例

8.3.12 可為 Null 的實值類型

可為 Null 的實值型別可以代表其基礎類型的所有值加上額外的 Null 值。 可為 Null 的實值型別會寫入 T?,其中 T 是基礎類型。 此語法是 的 System.Nullable<T>速記,而且這兩種形式可以交替使用。

相反地,不可為 Null 的實值型別是以外的任何實值型System.Nullable<T>別,T以及任何T?限製為不可為 Null 實值型別的類型參數(也就是具有實值型別條件約束的任何型別參數#15.2.5)。System.Nullable<T> 類型會指定的 T實值型別條件約束,這表示可為 Null 實值型別的基礎型別可以是任何不可為 Null 的實值型別。 可為 Null 實值型別的基礎型別不能是可為 Null 的實值型別或參考型別。 例如, int?? 是無效的類型。 在 \8.9涵蓋可為 Null 的參考型別。

可為 Null 實值類型的 T? 實體有兩個公用唯讀屬性:

  • HasValue類型的屬性bool
  • Value類型的屬性T

實例 HasValue ,其 true 稱為非 Null。 非 Null 實例包含已知的值,並 Value 傳回該值。

實例 HasValue ,其 false 稱為 null。 Null 實例具有未定義的值。 嘗試讀取 Value Null 實體的 ,會導致 System.InvalidOperationException 擲回 。 存取可為 Null 實例之 Value 屬性的程式稱為解除包裝

除了預設建構函式之外,每個可為 Null 的實值型 T? 別都有一個公用建構函式,其類型為的單一參數 T。 指定 類型的Tx,表單的建構函式調用

new T?(x)

會建立 Value 屬性為 x的非 Null 實例T?。 針對指定值建立可為 Null 實值型別的非 Null 實例的程式稱為 包裝

隱含轉換可從 null 常值到 T?~10.2.7) 和從 TT?~10.2.6)。

可為 Null 的實值型 T? 別不會實作任何介面 (~18)。 特別是,這表示它不會實作基礎類型 T 執行的任何介面。

8.3.13 Boxing 和 Unboxing

Boxing 和 unboxing 的概念提供 value_types 與 reference_types 之間的橋樑,方法是允許將value_type的任何值轉換成 和從 類型object轉換。 Boxing 和 unboxing 會啟用型別系統的統一檢視,其中任何類型的值最終都可以視為 object

Boxing 在 \10.2.9會詳細說明 Boxing,而 Unboxing 會在 \10.3.7詳細說明。

8.4 建構類型

8.4.1 一般

泛型型別宣告本身表示一種 未繫結的泛型型 別,用來透過套 用類型自變數來形成許多不同類型的「藍圖」。 類型自變數會緊接在泛型型別的名稱之後,以角括弧 (<>) 撰寫。 包含至少一個類型自變數的類型稱為建構型別 建構型別可用於大部分位置,其中可以顯示類型名稱的語言。 未系結的 泛型型別只能在typeof_expression 內使用(~12.8.18)。

建構的型別也可以在表達式中當做簡單名稱(~12.8.4)或存取成員時使用(~12.8.7)。

評估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
    }
}

end 範例

在namespace_or_type_name生產環境中,名稱查閱的詳細規則描述於 \7.8 這些生產環境中模棱兩可的解析說明於 •6.2.5即使type_name未直接指定類型參數,type_name還是可能會識別建構的類型。 當類型巢狀於泛型 class 宣告內,且包含宣告的實例類型隱含地用於名稱查閱時,就會發生這種情況({15.3.9.7)。

範例:

class Outer<T>
{
    public class Inner {...}

    public Inner i; // Type of i is Outer<T>.Inner
}

end 範例

非列舉建構型別不得作為 unmanaged_type~8.8)。

8.4.2 類型自變數

類型自變數清單中的每個自變數只是類型

type_argument_list
    : '<' type_arguments '>'
    ;

type_arguments
    : type_argument (',' type_argument)*
    ;   

type_argument
    : type
    | type_parameter nullable_type_annotation?
    ;

每個類型自變數應滿足對應類型參數上的任何條件約束(~15.2.5)。 參考型別自變數,其 Null 能力不符合類型參數的 Null 屬性滿足條件約束;不過,可能會發出警告。

8.4.3 開啟和關閉類型

所有類型都可以分類為開放式類型封閉類型。 開放式類型是包含類型參數的類型。 更明確地說:

  • 類型參數會定義開啟的類型。
  • 只有在數位類型是開啟型別時,數位類型是開啟的類型。
  • 只有在其中一或多個型別自變數是開啟型別時,建構型別是開啟型別。 如果建構的巢狀類型是開放式類型,而且只有當其類型自變數的一或多個類型自變數或其包含型別的型別自變數為開啟類型時,才會開啟類型。

封閉類型是不是開啟類型的類型。

在運行時間,泛型型別宣告內的所有程式代碼都會在封閉式建構型別的內容中執行,該型別是由將型別自變數套用至泛型宣告所建立。 泛型類型中的每個類型參數都會系結至特定的運行時間類型。 所有語句和表達式的運行時間處理一律會以封閉式類型發生,而開啟的類型只會在編譯時期處理期間發生。

如果兩個封閉的建構型別是從相同的未系結泛型型別建構而建構的,則識別可轉換型別(~10.2.2),而且每個對應型別自變數之間都有識別轉換。 對應的型別自變數本身可能是關閉可轉換身分識別的建構型別或 Tuple。 身分識別可轉換的封閉式建構類型會共用一組靜態變數。 否則,每個封閉式建構型別都有自己的靜態變數集。 由於開啟類型不存在於運行時間,因此沒有與開啟類型相關聯的靜態變數。

8.4.4 系結和未系結類型

未系結類型一詞 是指非泛型型 別或未系結泛型型別。 系結類型一詞是指非泛型型別或建構型別。

未系結類型是指類型宣告所宣告的實體。 未系結的泛型型別本身不是類型,而且不能當做變數的類型、自變數或傳回值,或是做為基底類型。 唯一可以參考未系結泛型型別的建構是 typeof 表達式 (~12.8.18)。

8.4.5 滿足條件約束

每當參考建構的類型或泛型方法時,會根據泛型類型或方法上宣告的類型參數條件約束來檢查提供的型別自變數(~15.2.5)。 針對每個 where 子句,對應至具名類型參數的類型自變數 A 會針對每個條件約束進行檢查,如下所示:

  • 如果條件約束是 class 型別、介面類型或型別參數,則 let 代表 C 該條件約束,其中包含所提供型別自變數,以取代條件約束中顯示的任何類型參數。 若要滿足條件約束,類型可轉換成下列其中一項類型C的情況A
    • 身分識別轉換 (•10.2.2
    • 隱含參考轉換 (\10.2.8
    • 方塊轉換 (~10.2.9),前提是該類型 A 是不可為 Null 的實值型別。
    • 從類型參數轉換成 C的隱含參考、Boxing 或類型參數A轉換。
  • 如果條件約束是參考型別條件約束 (class),則型 A 別應滿足下列其中一項:
    • A 是介面類型、類別類型、委派類型、數位類型或動態類型。

    注意System.ValueTypeSystem.Enum 是滿足此條件約束的參考型別。 end note

    • A 是已知為參考型別 (~8.2) 的類型參數。
  • 如果條件約束是實值型別條件約束 (struct),則類型 A 應滿足下列其中一項:
    • Astruct是類型或enum類型,但不是可為 Null 的實值型別。

    注意System.ValueTypeSystem.Enum 是不符合此條件約束的參考型別。 end note

    • A 是具有實值型別條件約束的類型參數(~15.2.5)。
  • 如果條件約束是建構函式條件約束 new(),則類型 A 不得為 abstract ,而且應該具有公用無參數建構函式。 如果下列其中一項成立,則會滿足此情況:
    • A 是實值型別,因為所有實值型別都有公用預設建構函式(~8.3.3)。
    • A 是具有建構函式條件約束的類型參數(~15.2.5)。
    • A 是具有實值型別條件約束的類型參數(~15.2.5)。
    • Aclass不是抽象的 ,而且包含沒有參數的明確宣告公用建構函式。
    • A 不是 abstract ,而且具有預設建構函式 (~15.11.5)。

如果指定的型別自變數未滿足一或多個型別參數的條件約束,就會發生編譯時期錯誤。

由於類型參數不會繼承,因此絕不會繼承條件約束。

範例:在下列範例中,需要在其類型參數T上指定條件約束, D T以符合基底 class B<T>所加之條件約束。 相反地, class E不需要指定條件約束,因為 List<T> 會針對任何 T實作 IEnumerable

class B<T> where T: IEnumerable {...}
class D<T> : B<T> where T: IEnumerable {...}
class E<T> : B<List<T>> {...}

end 範例

8.5 類型參數

類型參數是指定參數在運行時間系結之實值型別或參考型別的標識碼。

type_parameter
    : identifier
    ;

因為類型參數可以使用許多不同的類型自變數具現化,因此類型參數的作業和限制與其他類型稍有不同。

注意:這些包括:

  • 類型參數不能直接用來宣告基類 (~15.2.4.2) 或介面 (~18.2.4)。
  • 類型參數的成員查閱規則取決於條件約束,如果有的話,會套用至類型參數。 它們詳述於 •12.5
  • 類型參數的可用轉換取決於條件約束,如果有的話,會套用至類型參數。 它們詳述於 •10.2.1210.3.8
  • 常值 null 無法轉換成類型參數所指定的類型,除非已知類型參數是參考型別(~10.2.12)。 不過,您可以改用預設表達式 (~12.8.21)。 此外,除非型別參數具有實值型別條件約束,否則類型參數所指定類型的值可以使用!=~12.12.7) 與 null == 進行比較。
  • new表達式 (~12.8.17.2) 只有在類型參數受到constructor_constraint或實值型別條件約束的限制時,才能搭配類型參數使用。
  • 類型參數不能在屬性內的任何位置使用。
  • 類型參數不能用於成員存取(\12.8.7)或類型名稱(\7.8)來識別靜態成員或巢狀類型。
  • 類型參數不能當做unmanaged_type使用(~8.8)。

end note

作為類型,類型參數純粹是編譯時間建構。 在運行時間,每個類型參數都會系結至運行時間類型,該運行時間類型是藉由提供類型自變數給泛型類型宣告所指定的。 因此,在運行時間,以類型參數宣告的變數類型將會是封閉式建構類型 \8.4.3。 涉及類型參數的所有語句和表達式的運行時間執行,會使用提供做為該參數之型別自變數的類型。

8.6 運算式樹狀結構類型

表達式樹狀 架構允許 Lambda 運算式表示為數據結構,而不是可執行的程式代碼。 表達式樹狀結構是窗體 System.Linq.Expressions.Expression<TDelegate>的表達式樹狀結構類型,其中 TDelegate 是任何委派類型。 在此規格的其餘部分,這些型別會使用速記 Expression<TDelegate>來參考。

如果從 Lambda 運算式轉換為委派型 D別的轉換存在,則轉換也會存在運算式樹狀結構類型 Expression<TDelegate>。 雖然 Lambda 運算式轉換成委派類型會產生參考 Lambda 運算式可執行程式碼的委派,但轉換成表達式樹狀結構類型會建立 Lambda 表達式的表達式樹狀結構表示。 如需此轉換的詳細資訊,請參閱 \10.7.3

範例:下列程式表示 Lambda 運算式,同時以可執行程式代碼和表達式樹狀結構表示。 因為轉換存在 ,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的數據結構。

end 範例

Expression<TDelegate> 提供實例方法 Compile ,其會產生 型別 TDelegate的委派:

Func<int,int> del2 = exp.Compile();

叫用此委派會導致執行表達式樹狀結構所表示的程序代碼。 因此,鑒於上述定義, deldel2 是相等的,而且下列兩個語句的效果相同:

int i1 = del(1);
int i2 = del2(1);

執行此程式代碼之後, i1i2 者都會有 值 2

所提供的 API 介面是 Expression<TDelegate> 實作定義,超出上述方法的需求 Compile

注意:雖然針對表達式樹狀架構提供的 API 詳細數據是實作定義,但實作預期會:

  • 啟用程式代碼來檢查和回應從 Lambda 運算式轉換而建立的表達式樹狀結構
  • 在使用者程式代碼中以程序設計方式建立表達式樹狀結構

end note

8.7 動態類型

此類型dynamic使用動態系結,如 •12.3.2 中所述,而不是所有其他類型所使用的靜態系結。

此類型 dynamic 會視為與 object 相同,但下列方面除外:

由於這種等價,下列專案會保留:

  • 有隱含身分識別轉換
    • 和之間的objectdynamic
    • 在以 取代 dynamic 時相同建構的類型之間 object
    • 在取代 dynamic 為時相同 Tuple 類型之間的 object
  • 對與之間的 object 隱含與明確轉換也會套用至 和 從 dynamic
  • 將取代dynamicobject為 時相同的簽章會被視為相同的簽章。
  • 此類型 dynamic 在運行時間與類型 object 不區分。
  • 類型的 dynamic 表達式稱為 動態表達式

8.8 非受控類型

unmanaged_type
    : value_type
    | pointer_type     // unsafe code support
    ;

unmanaged_type是不是reference_typetype_parameter或建構型別的任何類型,而且不包含類型不是unmanaged_type的實例字段。 換句話說, unmanaged_type 是下列其中一項:

  • sbytebyteshort、、ushortintdecimalcharuintlongdoublefloatulong、 或 。bool
  • 任何 enum_type
  • 任何使用者定義struct_type不是建構的類型,且只包含 unmanaged_type的實例欄位
  • 在不安全的程序代碼中(~23.2),任何 pointer_type~23.3)。

8.9 參考型別和 Null 性

8.9.1 一般

可為 Null 的參考型別是藉由將nullable_type_annotation?) 附加至不可為 Null 的參考型別來表示。 不可為 Null 的參考型別與其對應的可為 Null 型別之間沒有語意差異,兩者都可以是對象的參考或 null。 nullable_type_annotation是否存在會宣告表達式是否要允許 Null 值。 當表達式未根據該意圖使用時,編譯程式可能會提供診斷。 表達式的 Null 狀態定義於 •8.9.5 中。 身分識別轉換存在於可為 Null 的參考型別及其對應的不可為 Null 參考型別(~10.2.2)。

參考型別有兩種可為 Null 的形式:

  • nullable可以指派null可為 nullable-reference-type。 其預設 Null 狀態為 可能為 null
  • 不可為 Null:不可為 Null 的參考不應指派null值。 其預設 Null 狀態不是 null

注意:R? 的類型R會以相同的基礎類型 R來表示。 該基礎型別的變數可以包含對象的參考或值 null,表示「沒有參考」。 end note

可為 Null 參考型別與其對應的不可為 Null 參考型別之間的語法區別可讓編譯程式產生診斷。 編譯程式必須允許nullable_type_annotation ,如8.2.1 中所定義。 診斷必須限制為警告。 不存在可為 Null 的批注,或可為 Null 內容的狀態都無法變更程式的編譯時間或運行時間行為,但編譯期間所產生的任何診斷訊息變更除外。

8.9.2 不可為 Null 的參考型別

不可為 Null 的參考型別是窗體T的參考型別,其中 T 是型別的名稱。 不可為 Null 變數的預設 Null 狀態不是 Null。 當需要非 Null 值的運算式,可能會產生警告。

8.9.3 可為 Null 的參考型別

表單 T? 的參考型別(例如 string?) 是 可為 Null 的參考型別。 可為 Null 變數 的預設 Null 狀態可能是 null。 批註 ? 指出此類型的變數可為 Null 的意圖。 編譯程式可以辨識這些意圖來發出警告。 停用可為 Null 的註釋內容時,使用此批註可能會產生警告。

8.9.4 可為 Null 的內容

8.9.4.1 一般

原始程式碼的每一行都有可為 Null 的內容 可為 Null 的內容控件可為 Null 註釋的批註和警告旗標(~8.9.4.3)和可為 Null 的警告(~8.9.4.4)。 每個旗標都可以 啟用停用。 編譯程式可以使用靜態流程分析來判斷任何參考變數的 Null 狀態。 參考變數的 Null 狀態 (~8.9.5不是 Null、可能是 null預設值

原始碼中可以透過可為 Null 的指示詞 (~6.5.9) 和/或透過原始程式碼外部的一些實作特定機制來指定可為 Null 的內容。 如果使用這兩種方法,可為 Null 的指示詞會取代透過外部機制所做的設定。

可為 Null 內容的預設狀態是已定義實作。

在整個規格中,不包含可為 Null 指示詞的所有 C# 程式代碼,或未針對目前可為 Null 的內容狀態進行語句,應該假設已使用已啟用註釋和警告的可為 Null 內容進行編譯。

注意: 停用這兩個旗標的可為 Null 內容符合參考型別先前的標準行為。 end note

8.9.4.2 可為 Null 的停用

當警告和批註旗標同時停用時,可為 Null 的內容會 停用

停用可為 Null 的內容時:

  • 當未批注參考型別的變數使用 或指派 的值 null初始化時,不應產生任何警告。
  • 如果參考型別的變數可能具有 Null 值,則不會產生任何警告。
  • 對於任何參考型別T,中的T?批注?會產生訊息,且型T?別與 T相同。
  • 對於任何類型參數條件約束where T : C?,中的C?註釋?會產生訊息,而型C?別與 C相同。
  • 對於任何類型參數條件約束where T : U?,中的U?註釋?會產生訊息,而型U?別與 U相同。
  • 泛型條件約束 class? 會產生警告訊息。 類型參數必須是參考型別。

    注意:此訊息的特性為「資訊」而非「警告」,以免與不相關的可為 Null 警告設定的狀態混淆。 end note

  • Null 放棄運算符 !^12.8.9) 沒有作用。

範例:

#nullable disable annotations
string? s1 = null;    // Informational message; ? is ignored
string s2 = null;     // OK; null initialization of a reference
s2 = null;            // OK; null assignment to a reference
char c1 = s2[1];      // OK; no warning on dereference of a possible null;
                      //     throws NullReferenceException
c1 = s2![1];          // OK; ! is ignored

end 範例

8.9.4.3 可為 Null 的註釋

停用警告旗標並啟用批註旗標時,可為 Null 的內容為 註釋

當可為 Null 的內容為 註釋時:

  • 對於任何參考型別 ,中的註釋表示可為 Null 的類型T,而未批註T則為不可為 Null。T? ? T?
  • 不會產生任何與可為 Null 相關的診斷警告。
  • Null 放棄運算符 !^12.8.9) 可能會改變其操作數的已分析 Null 狀態,以及產生編譯時間診斷警告。

範例:

#nullable disable warnings
#nullable enable annotations
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; warnings are disabled
s2 = null;            // OK; warnings are disabled
char c1 = s2[1];      // OK; warnings are disabled; throws NullReferenceException
c1 = s2![1];          // No warnings

end 範例

8.9.4.4 可為 Null 的警告

啟用警告旗標並停用批註旗標時,可為 Null 的內容會是 警告

當可為 Null 的內容為 警告時,編譯程式可以在下列情況下產生診斷:

  • 已判斷為 null 的參考變數會被取值。
  • 不可為 Null 類型的參考變數會指派給可能為 null 的運算式。
  • ?用來記下可為 Null 的參考型別。
  • Null 表示運算符 !^12.8.9) 用來將其操作數的 Null 狀態設定為 非 Null

範例:

#nullable disable annotations
#nullable enable warnings
string? s1 = null;    // OK; ? makes s2 nullable
string s2 = null;     // OK; null-state of s2 is "maybe null"
s2 = null;            // OK; null-state of s2 is "maybe null"
char c1 = s2[1];      // Warning; dereference of a possible null;
                      //          throws NullReferenceException
c1 = s2![1];          // The warning is suppressed

end 範例

啟用 8.9.4.5 可為 Null

啟用警告旗標和批註旗標時,會啟用可為 Null 的內容

啟用可為 Null 的內容時:

  • 對於任何參考型別 ,中的T?註釋會建立T?可為 Null 的類型T,而未批註T則為?不可為 Null。
  • 編譯程式可以使用靜態流程分析來判斷任何參考變數的 Null 狀態。 啟用可為 Null 的警告時,參考變數的 Null 狀態 (~8.9.5不是 Null、可能是 null,或可能是預設值
  • Null 表示運算符 !^12.8.9) 會將其操作數的 Null 狀態設定為 非 Null
  • 如果類型參數的 Null 屬性不符合其對應類型自變數的 Null 屬性,編譯程式可能會發出警告。

8.9.5 Nullabilities 和 Null 狀態

編譯程式不需要執行任何靜態分析,也不需要產生任何與可為 Null 相關的診斷警告。

這個子程式的其他部分是有條件的規範。

產生診斷警告的編譯程式符合這些規則。

每個運算式都有三 個 Null 狀態的其中一個:

  • 可能為 null:表達式的值可能會評估為 null。
  • 也許預設值:表達式的值可能會評估為該類型的預設值。
  • not null:表達式的值不是 Null。

表達式的預設 Null 狀態取決於其類型,以及在宣告批註旗標時的狀態:

  • 可為 Null 參考型別的預設 Null 狀態為:
    • 當其宣告在啟用註釋旗標的文字中時,可能為 null。
    • 當其宣告在已停用註釋旗標的文字中時,則為 Null。
  • 不可為 Null 參考型別的預設 Null 狀態不是 Null。

注意:類型為不可為 Null 的類型時,可能的默認 狀態會與不受限制的類型參數搭配使用,例如 string ,而表達式 default(T) 是 Null 值。 因為 null 不在不可為 Null 類型的網域中,狀態可能是預設值。 end note

當不可為 Null 參考型別的變數 (~9.2.1) 初始化或指派給表達式時,可以在啟用註釋旗標的文字中宣告該變數時,產生診斷。

範例:請考慮下列方法,其中參數可為 Null,且該值指派給不可為 Null 的類型:

#nullable enable
public class C
{
    public void M(string? p)
    {
        // Assignment of maybe null value to non-nullable variable
        string s = p;
    }
}

編譯程式可能會發出警告,其中可能為 Null 的參數會指派給不應該為 Null 的變數。 如果在指派前先檢查參數,編譯程式可能會使用該參數進行可為 Null 的狀態分析,而不會發出警告:

#nullable enable
public class C
{
    public void M(string? p)
    {
        if (p != null)
        {
            string s = p;
            // Use s
        }
    }
}

end 範例

編譯程式可以在分析中更新變數的 Null 狀態。

範例:編譯程式可以選擇根據程式中的任何語句來更新狀態:

#nullable enable
public void M(string? p)
{
    // p is maybe-null
    int length = p.Length;

    // p is not null.
    string s = p;

    if (s != null)
    {
        int l2 = s.Length;
    }
    // s is maybe null
    int l3 = s.Length;
}

在上述範例中,編譯程式可能會決定在語句 int length = p.Length;之後,null 狀態 p 不是 null。 如果它是 Null,該語句會擲回 NullReferenceException。 如果程式代碼前面加上 if (p == null) throw NullReferenceException(); ,這與行為類似,不同之處在於撰寫的程式代碼可能會產生警告,其用途是警告可能會隱含擲回例外狀況。

稍後在方法中,程式代碼會檢查 s 不是 Null 參考。 的 Null 狀態 s 可以在 Null 核取的區塊關閉之後變更為 null。 編譯程式可以推斷可能是 Null, s 因為程式代碼已撰寫來假設它可能是 Null。 一般而言,當程式代碼包含 Null 檢查時,編譯程式可能會推斷該值可能是 Null。end 範例

範例:編譯程式可以將屬性 (~15.7) 視為具有狀態的變數,或將屬性視為獨立的 get 和 set 存取子 (~15.7.3)。 換句話說,編譯程式可以選擇寫入屬性是否會變更讀取屬性的 Null 狀態,或讀取屬性變更該屬性的 Null 狀態。

class Test
{
    private string? _field;
    public string? DisappearingProperty
    {
        get
        {
               string tmp = _field;
               _field = null;
               return tmp;
        }
        set
        {
             _field = value;
        }
    }

    static void Main()
    {
        var t = new Test();
        if (t.DisappearingProperty != null)
        {
            int len = t.DisappearingProperty.Length;
        }
    }
}

在上一個範例中,讀取 時,的備份欄位 DisappearingProperty 會設定為 null。 不過,編譯程式可能會假設讀取屬性不會變更該表達式的 Null 狀態。 end 範例

有條件規範的文字結尾