C# 型別系統

C# 是強型別語言。 每個變數和常數都有型別,如同每個會評估為值的運算式一般。 每個方法宣告都會針對每個輸入參數和傳回值指定名稱、型別和型別 (值、參考或輸出)。 .NET 類別庫定義內建的數值型別和複雜類型,這些型別代表各種不同的建構。 其中包括檔案系統、網路連線、物件集合和陣列,以及日期。 一般 C# 程式會使用類別庫的型別和使用者定義的型別,模型化程式的問題領域特有概念。

可儲存在型別中的資訊包括下列各項:

  • 型別的變數需要的儲存空間。
  • 它可以代表的最大值和最小值。
  • 它所包含的成員 (方法、欄位、事件等等)。
  • 它繼承自的基底型別。
  • 其實作的介面。
  • 允許的作業類型。

編譯器會使用型別資訊,來確認在程式碼中執行的所有作業全都是「型別安全」。 例如,如果您宣告型別為 int 的變數,編譯器會允許您使用這個變數去做加減運算。 如果您嘗試針對型別為 bool 的變數執行相同作業,編譯器會產生錯誤,如下列範例所示︰

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

注意

C 和 C++ 開發人員要注意在 C# 中,bool 不能轉換為 int

編譯器會將型別資訊視為中繼資料內嵌至可執行檔。 通用語言執行平台 (CLR) 會在執行階段使用該中繼資料,以在它配置和回收記憶體時,進一步保證型別安全。

在變數宣告中指定類型

在程式中宣告變數或常數時,您必須指定其型別,或使用 var 關鍵字以讓編譯器推斷其型別。 下列範例示範一些變數宣告,使用內建的數字型別和複雜的使用者定義型別︰

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = [0, 1, 2, 3, 4, 5];
var query = from item in source
            where item <= limit
            select item;

在方法宣告中指定方法參數和傳回值的型別。 下列簽章顯示的方法要求將 int 做為輸入引數且會傳回字串︰

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = ["Spencer", "Sally", "Doug"];

宣告變數之後,您無法以新的型別重新宣告變數,而且無法指派與其宣告型別不相容的值。 例如,您無法宣告 int 並將 true 的布林值指派至其中。 不過,可以將值轉換為其他型別,例如,指派給新的變數,或做為方法引數傳遞時。 編譯器會自動執行不會造成資料遺失的「型別轉換」作業。 而可能導致資料遺失的轉換在原始程式碼中需要有 cast

如需詳細資訊,請參閱轉換和型別轉換

內建類型

C# 提供一組標準內建型別。 這些代表整數、浮點數值、布林運算式、文字字元、十進位值及其他資料型別。 另外還有內建的 stringobject 型別。 這些型別都可供您在任何 C# 程式中使用。 如需內建型別的清單,請參閱內建型別

自訂類型

您可以使用 structclassinterfaceenumrecord 建構來建立您自己的自訂型別。 .NET 類別庫本身是自訂型別集合,可供您用於自己的應用程式中。 根據預設,類別庫中最常使用的型別可用於任何 C# 程式中。 只有當您明確地將專案參考新增至定義所在的組件時,才能使用其他型別。 編譯器在有該組件的參考之後,您可以針對在原始程式碼的那個組件中宣告的型別宣告變數 (和常數) 。 如需詳細資訊,請參閱 .NET 類別庫

一般型別系統

請務必了解 .NET 中有關型別系統的兩個基本概念:

  • 它支援繼承原則。 型別可以衍生自稱為「基底型別」的其他型別。 衍生的型別會繼承 (有部份限制) 基底型別的方法、屬性和其他成員。 基底型別同樣可以衍生自一些其他型別,所衍生的型別會繼承其繼承階層架構中兩個基底型別的成員。 所有類型 (包括 System.Int32 這類內建數字類型 (C# 關鍵字:int)) 最終衍生自單一基底類型,即 System.Object (C# 關鍵字:object)。 這種統一型別階層架構稱為一般型別系統 (CTS)。 如需 C# 中有關繼承的詳細資訊,請參閱繼承
  • CTS 中的每個型別都會定義為「實值型別」或「參考型別」。 這包括 .NET 類別庫中的所有自訂型別以及您自己的使用者定義型別。 您使用 struct 關鍵字定義的型別都是實值型別;所有內建的數字型別都是 structs。 您使用 classrecord 關鍵字定義的型別為參考型別。 參考型別和實值型別有不同的編譯時期規則和不同的執行階段行為。

下圖顯示 CTS 中的實值型別和參考型別之間的關聯性。

Screenshot that shows CTS value types and reference types.

注意

您可以看到最常使用的型別全都整理在 System 命名空間中。 不過,其中包含型別的命名空間和實值型別或參考型別毫無關聯。

類別和結構是 .NET 中一般型別系統的兩個基本建構。 每一個基本上都是封裝一組屬於相同邏輯單元之資料和行為的資料結構。 資料和行為是類別、結構或記錄的成員。 如本文稍後所列,成員包含其方法、屬性、事件等等。

類別、結構或記錄宣告就像是用來在執行階段建立執行個體或物件的藍圖。 如果您定義名為 Person 的類別、結構或記錄,Person 將會是型別的名稱。 如果您宣告並初始化型別 Person 的變數 pp 即為 Person 的物件或執行個體。 您可以建立多個相同 Person 型別的執行個體,且每個執行個體在其屬性與欄位中都可以有不同的值。

類別是參考型別。 當建立型別的物件時,物件指派至的變數僅會保留該記憶體的參考。 當物件參考指派至新的變數時,新的變數會參考到原始物件。 透過某個變數所做的變更會反映在其他變數中,因為它們都參考相同的資料。

結構是實值型別。 當建立結構時,結構指派至的變數會保留結構的實際資料。 將結構指派至新的變數時,將會複製結構。 因此,新的變數和原始變數會各自包含一份相同的資料。 針對其中一個複本所做的變更,並不會影響到另一個複本。

記錄型別可以是參考型別 (record class) 或實值型別 (record struct)。 記錄型別包括支援實值相等的方法。

一般而言,類別是用來建立更複雜行為的模型。 類別通常會儲存在建立類別物件之後要修改的資料。 結構最適合小型資料結構。 結構通常會儲存在建立結構之後要修改的資料。 記錄型別是具有其他編譯器合成成員的資料結構。 記錄通常會儲存在建立物件之後要修改的資料。

值類型

實值型別衍生自 System.ValueType,該型別又衍生自 System.Object。 衍生自 System.ValueType 的型別在 CLR 中具有特殊行為。 實值型別變數直接包含其值。 會在宣告變數的任何內容中內嵌配置結構記憶體。 實值型別變數不會有任何個別的堆積配置或記憶體回收額外負荷。 您可以宣告作為實值型別 record struct 的型別,並包含記錄的合成成員。

實值型別有兩種類別︰structenum

內建的數字型別為結構,而您可以存取其欄位和方法︰

// constant field on type byte.
byte b = byte.MaxValue;

但您會將其當做簡單的非彙總型別來宣告並將值指派至其中︰

byte num = 0xA;
int i = 5;
char c = 'Z';

實值型別是密封的。 您無法從任何實值型別衍生型別,例如 System.Int32。 您無法定義要繼承自任何使用者定義類別或結構的結構,因為結構只能繼承自 System.ValueType。 不過,結構可以實作一個或多個介面。 您可以將結構型別轉換成其實作的任何介面型別。 此轉換導致 boxing 作業將結構包裝在受控堆積上的參考型別物件內。 當您將實值型別傳遞至接受 System.Object 或任何介面型別做為輸入參數的方法時,就會發生 Boxing 作業。 如需詳細資訊,請參閱 Boxing 和 Unboxing

您使用 struct 關鍵字來建立您自己自訂的實值型別。 一般來說,會使用結構做為一小組相關變數的容器,如下列範例所示︰

public struct Coords
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

如需結構的詳細資訊,請參閱結構型別。 如需實值型別的詳細資訊,請參閱實值型別

實值型別的另一種類別是 enum。 列舉會定義一組具名的整數常數。 例如,.NET 類別庫中的 System.IO.FileMode 列舉包含一組指定該如何開啟檔案的具名常數整數。 其定義方式如下列範例所示:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

System.IO.FileMode.Create 常數的值為 2。 不過,使用者在閱讀原始程式碼時,名稱會更有意義,也因此最好使用列舉,而不要使用常數常值數字。 如需詳細資訊,請參閱System.IO.FileMode

所有的委派都繼承自 System.Enum,該列舉又繼承自 System.ValueType。 所有適用於結構的規則,也適用於列舉。 如需有關列舉的詳細資訊,請參閱列舉型別

參考型別

定義為 classrecorddelegate、陣列或 interface 的型別是 reference type

宣告 reference type 的變數時,其會包含值 null,直到您使用該型別的執行個體來進行指派,或使用 new 運算子進行建立。 下列範例示範類別的建立和指派:

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

無法使用 new 運算子直接具現化 interface。 相反地,請建立並指派實作介面之類別的執行個體。 請考慮下列範例:

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

建立物件時,記憶體會配置在受控堆積上。 變數只保留物件位置的參考。 受控堆積上的型別在配置時和回收時都需要額外負荷。 記憶體回收是 CLR 的自動記憶體管理功能,該功能可執行回收。 不過,記憶體回收也已獲得高度最佳化,因此在大部分情節下並不會產生效能問題。 如需有關記憶體回收的詳細資訊,請參閱自動記憶體管理

所有陣列都是參考型別,即使其元素都是實值型別。 陣列會隱含衍生自 System.Array 類別。 如下列範例所示,您會利用 C# 所提供的簡化語法來宣告及使用陣列:

// Declare and initialize an array of integers.
int[] nums = [1, 2, 3, 4, 5];

// Access an instance property of System.Array.
int len = nums.Length;

參考型別完全支援繼承。 當您建立類別時,可以繼承自未定義為密封的任何其他介面或類別。 其他類別可以繼承自類別,並覆寫虛擬方法。 如需如何為您自己建立類別的詳細資訊,請參閱類別、結構和記錄。 如有關繼承和虛擬方法的詳細資訊,請參閱繼承

常值型別

在 C# 中,常值會接收來自編譯器的型別。 您可以在數字後面附加一個字母,指定應如何輸入數值常值。 例如,若要指定應該將值 4.56 視為 float 時,請在數字之後附加 "f" 或 "F"︰4.56f。 如果未附加任何字母時,編譯器會推斷其型別為常值。 如需可以使用字母尾碼指定哪些型別的詳細資訊,請參閱整數數值型別浮點數數值型別

因為輸入的是常值且所有型別最終都衍生自 System.Object,所以您可以如下程式碼所示來撰寫和編譯程式碼:

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

泛型類型

可使用一或多個「型別參數」宣告的型別,可做為實際型別的預留位置 (具象型別)。 當用戶端程式碼建立型別的執行個體時,便會提供具象型別。 這類的型別稱為「泛型型別」。 例如,.NET 型別 System.Collections.Generic.List<T> 有一個型別參數,依慣例命名為 T。 建立該型別的執行個體時,您會指定該清單將包含的物件型別,例如 string

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

使用型別參數讓您能夠重複使用相同的類別來保存任何元素型別,而不需要將每個元素都轉換成 object。 泛型集合類別則稱為「強型別集合」,因為編譯器知道集合元素的特定型別,如果您嘗試將整數加入至上一個範例中的 stringList 物件,便會在編譯時期引發錯誤。 如需詳細資訊,請參閱泛型

隱含型別、匿名型別以及可為 Null 的實值型別

您可以使用 var 關鍵字,隱含地輸入本機變數 (但不是類別成員)。 變數還是會在編譯時期收到型別,但其是由編譯器所提供的型別。 如需詳細資訊,請參閱隱含型別區域變數

為一組您不想要儲存或在方法界限外傳遞的簡單相關值,建立簡單的具名型別,可能不太方便。 為此,您可以建立「匿名型別」。 如需詳細資訊,請參閱匿名型別

一般的實值型別不能為 null 值。 不過,您可以在該型別後面添加 ?,建立可為 null 的實值型別。 例如,int? 就是也能有 null 值的 int 型別。 可為 Null 的實值型別是泛型結構型別 System.Nullable<T> 的執行個體。 當您要在資料庫之間來回傳遞的資料數值可能為 null 時,可為 Null 的實值型別會特別有用。 如需詳細資訊,請參閱可為 Null 的實值型別

編譯時間型別和執行階段型別

變數可以有不同的編譯時間和執行階段型別。 compile-time 型別是原始程式碼中變數的宣告或推斷型別。 執行階段型別是該變數所參考之執行個體的型別。 如下列範例所示,這兩種型別通常是相同的:

string message = "This is a string of characters";

如下列兩個範例所示,在其他案例中,編譯時間型別有所不同:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

在上述兩個範例中,執行階段型別是 string。 編譯時間型別是第一行中的 object,和第二行中的 IEnumerable<char>

如果變數的兩個型別不同,請務必了解編譯時間型別和執行階段型別適用的時機。 編譯時間型別會決定編譯器所採取的所有動作。 這些編譯器動作包括方法呼叫解析、多載解析,以及可用的隱含和明確轉換。 執行階段型別會決定在執行階段解析的所有動作。 這些執行階段動作包括分派虛擬方法呼叫、評估 isswitch 運算式,以及其他型別測試 API。 若要進一步了解程式碼如何與型別互動,請辨識哪些動作適用於哪個型別。

如需詳細資訊,請參閱下列文章:

C# 語言規格

如需詳細資訊,請參閱<C# 語言規格>。 語言規格是 C# 語法及用法的限定來源。