內建參考型別 (C# 參考)

C# 有許多內建參考類型。 它們具有關鍵字或運算子,是 .NET 程式庫中類型的同義字。

物件型別

object 類型是 System.Object 在 .NET 中的別名。 在 C# 的統一型別系統中,所有類型 (預先定義和使用者定義的、參考型別和實值型別) 都會直接或間接繼承自 System.Object。 您可以將任何型別的值指派給 object 型別的變數。 任何 object 變數都可以使用常值 null 指派給其預設值。 當實值型別的變數轉換成物件時,即稱之為「Boxed」。 當類型 object 的變數轉換成實值型別時,即稱之為「Unboxed」。 如需詳細資訊,請參閱 Boxing 和 Unboxing

字串型別

string 類型代表零或多個 Unicode 字元序列。 stringSystem.String 在 .NET 中的別名。

雖然 string 是參考型別,但是會定義等號比較運算子 ==!= 來比較 string 物件的值,而不是參考。 比對值的等式能讓字串等式的測試更為直覺性。 例如:

string a = "hello";
string b = "h";
// Append to contents of 'b'
b += "ello";
Console.WriteLine(a == b);
Console.WriteLine(object.ReferenceEquals(a, b));

因為字串的內容相等,所以上列範例依序顯示 "True" 和 "False",但 ab 不參考相同的字串執行個體。

+ 運算子會串連字串:

string a = "good " + "morning";

上述程式碼會建立包含 "good morning" 的字串物件。

字串「不可變」,在建立物件之後,就無法變更字串物件的內容。 例如,當您撰寫此程式碼時,編譯器實際上會建立新的字串物件,以保存新序列的字元,並且會將新物件指派給 b。 已配置給 b 的記憶體 (當其包含字串 "h" 時) 即適用於記憶體回收。

string b = "h";
b += "ello";

[] 運算子可以用於唯讀存取字串中的各個字元。 有效的索引值從 0 開始,且必須小於字串的長度:

string str = "test";
char x = str[2];  // x = 's';

[] 運算子也可以類似的方式,用來逐一查看字串中的每個字元:

string str = "test";

for (int i = 0; i < str.Length; i++)
{
  Console.Write(str[i] + " ");
}
// Output: t e s t

字串常值

字串常值的類型為 string,且可以用三種形式撰寫:原始字串、加引號和逐字。

從 C# 11 開始,可以使用「原始字串常值」。 原始字串常值可包含任意文字,而不需要逸出序列。 原始字串常值可包含空白字元和新行、內嵌引號以及其他特殊字元。 原始字串常值會以至少三個雙引號括住 ("""):

"""
This is a multi-line
    string literal with the second line indented.
"""

甚至可以包含三個 (或更多) 一連串的雙引號字元。 如果您的文字需要內嵌一連串的引號,可以視需要加上更多引號作為原始字串常值的開頭與結尾:

"""""
This raw string literal has four """", count them: """" four!
embedded quote characters in a sequence. That's why it starts and ends
with five double quotes.

You could extend this example with as many embedded quotes as needed for your text.
"""""

原始字串常值通常會在和內嵌文字不同的各自兩行,出現開頭和結尾的一連串引號。 多行原始字串常值,支援本身被引號括住的字串:

var message = """
"This is a very important message."
""";
Console.WriteLine(message);
// output: "This is a very important message."

當開頭和結尾引號位於不同行時,接在開頭引號之後但在結尾引號之前的新行,不會包含在最終內容中。 結尾的一連串引號,代表字串常值最左邊的資料行。 您可以縮排原始字串常值,以配合整體程式碼格式:

var message = """
    "This is a very important message."
    """;
Console.WriteLine(message);
// output: "This is a very important message."
// The leftmost whitespace is not part of the raw string literal

結尾一連串引號右側的資料行會保留。 此行為可形成資料格式 (JSON、YAML 或 XML 等) 的原始字串,如下範例所示:

var json= """
    {
        "prop": 0
    }
    """;

如果任何文字行超過了結尾一連串引號的左邊,編譯器就會發出錯誤。 開頭和結尾的一連串引號,可位於同一行,前提是字串常值不會以引號字元作為開頭或結尾:

var shortText = """He said "hello!" this morning.""";

您可以將原始字串常值與字串插補相合併,在輸出字串中包含引號字元和大括弧。

以引號括住的字串常值是以雙引號 (") 括住:

"good morning"  // a string literal

字串常值可以包含任何字元常值。 包含逸出序列。 下列範例使用逸出序列 \\ 表示反斜線、\u0066 表示字母 f,而 \n 表示新行字元。

string a = "\\\u0066\n F";
Console.WriteLine(a);
// Output:
// \f
//  F

注意

逸出代碼 \udddd (其中 dddd 是四位數字) 代表 Unicode 字元 U+dddd。 也會辨識八位數 Unicode 逸出代碼︰\Udddddddd

逐字字串常值的開頭為 @,也會用雙引號括住。 例如:

@"good morning"  // a string literal

逐字字串的優點,是「不會」處理逸出序列,讓撰寫輕鬆不少。 例如,下列文字符合完整的 Windows 檔案名稱:

@"c:\Docs\Source\a.txt"  // rather than "c:\\Docs\\Source\\a.txt"

若要在括了 @ 的字串中包含雙引號,請使用兩個引號︰

@"""Ahoy!"" cried the captain." // "Ahoy!" cried the captain.

UTF-8 字串常值

.NET 中的字串會使用 UTF-16 編碼儲存。 UTF-8 是 Web 通訊協定和其他重要程式庫的標準。 從 C# 11 開始,可以對字串常值新增尾碼 u8,指定使用 UTF-8 編碼。 UTF-8 常值會儲存為 ReadOnlySpan<byte> 物件。 UTF-8 字串常值的自然類型為 ReadOnlySpan<byte>。 使用 UTF-8 字串常值,可建立比宣告對等 System.ReadOnlySpan<T> 更清楚的宣告,如下列程式碼所示:

ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
ReadOnlySpan<byte> AuthStringLiteral = "AUTH "u8;

若要將 UTF-8 字串常值儲存為陣列,需要使用 ReadOnlySpan<T>.ToArray(),將包含常值的位元組,複製到可變動陣列:

byte[] AuthStringLiteral = "AUTH "u8.ToArray();

UTF-8 字串常值不是編譯時期的常數;它們是執行階段常數。 因此,不能用作為選用參數的預設值。 UTF-8 字串常值無法與字串插補合併使用。 在同一個字串運算式上,無法同時使用 $ 權杖和 u8 尾碼。

委派型別

委派類型的宣告類似方法簽章。 它具有傳回值以及任意類型之任何數目的參數:

public delegate void MessageDelegate(string message);
public delegate int AnotherDelegate(MyType m, long num);

在 .NET 中,System.ActionSystem.Func 型別提供許多常見委派的泛型定義。 您多半不需要定義新的自訂委派型別。 反之,您可以建立所提供之泛型型別的具現化。

delegate 是可用來封裝具名或匿名方法的參考型別。 委派類似 C++ 中的函式指標,但委派是型別安全而且安全的。 如需委派的應用程式,請參閱委派泛型委派。 委派是事件的基礎。 委派的具現化方式是將它與具名或匿名方法建立關聯。

委派必須使用具有相容傳回型別和輸入參數的方法或 Lambda 運算式進行具現化。 如需方法簽章中所允許變異程度的詳細資訊,請參閱 Variance in Delegates (委派中的變異數)。 若與匿名方法搭配使用,則會一起宣告要與它建立關聯的委派和程式碼。

執行階段涉及的委派類型因為變異轉換而不同時,委派組合或移除會因為執行階段的例外狀況而失敗。 下列範例示範失敗的情況:

Action<string> stringAction = str => {};
Action<object> objectAction = obj => {};
  
// Valid due to implicit reference conversion of
// objectAction to Action<string>, but may fail
// at run time.
Action<string> combination = stringAction + objectAction;

藉由建立新的委派物件,可以建立具有正確執行階段類型的委派。 下列範例示範如何為之前的範例運用此因應措施。

Action<string> stringAction = str => {};
Action<object> objectAction = obj => {};
  
// Creates a new delegate instance with a runtime type of Action<string>.
Action<string> wrappedObjectAction = new Action<string>(objectAction);

// The two Action<string> delegate instances can now be combined.
Action<string> combination = stringAction + wrappedObjectAction;

您可以宣告使用類似語法的函式指標。 函式指標會使用 calli 指令,而不是具現化委派類型並呼叫虛擬 Invoke 方法。

動態型別

dynamic 型別指出使用變數和對其成員的參考,會略過編譯時間型別檢查。 相反地,這些作業會在執行階段解決。 dynamic 類型會簡化 Office Automation API 這類 COM API 的存取、IronPython 程式庫這類動態 API 的存取,以及 HTML 文件物件模型 (DOM) 的存取。

在大多數情況下,dynamic 類型的行為與 object 類型類似。 特別是,任何非 Null 運算式都可轉換成 dynamic 型別。 dynamic 類型與 object 的不同之處在於,包含類別 dynamic 的運算式作業,編譯器不會對其進行解析或類型檢查。 編譯器會將作業資訊封裝在一起,而且稍後在執行階段會使用這項資訊來評估作業。 在此程序期間,會將 dynamic 類型的變數編譯為 object 類型的變數。 因此,dynamic 類型只存在於編譯時期,而非執行階段。

下列範例會對照 dynamic 類型的變數與 object 類型的變數。 若要在編譯時期驗證每個變數的類型,請將滑鼠指標放在 WriteLine 陳述式中的 dynobj 上方。 將下列程式碼複製到可使用 IntelliSense 的編輯器。 IntelliSense 會顯示「動態」來表示 dyn,並顯示「物件」來表示 obj

class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object obj = 1;

        // Rest the mouse pointer over dyn and obj to see their
        // types at compile time.
        System.Console.WriteLine(dyn.GetType());
        System.Console.WriteLine(obj.GetType());
    }
}

WriteLine 陳述式會顯示執行階段類型 dynobj。 此時,兩者都有相同的類型:整數。 此時會產生下列輸出:

System.Int32
System.Int32

若要查看 dynobj 在編譯時期的差異,請在上述範例的宣告與 WriteLine 陳述式之間新增下列兩行。

dyn = dyn + 3;
obj = obj + 3;

嘗試新增運算式 obj + 3 中的整數和物件時報告編譯器錯誤。 不過,不會回報 dyn + 3 的錯誤。 因為 dyn 的類型是 dynamic,所以在編譯時期不會檢查包含 dyn 的運算式。

下列範例會在數個宣告中使用 dynamicMain 方法也會對照編譯時期類型檢查與執行階段類型檢查。

using System;

namespace DynamicExamples
{
    class Program
    {
        static void Main(string[] args)
        {
            ExampleClass ec = new ExampleClass();
            Console.WriteLine(ec.ExampleMethod(10));
            Console.WriteLine(ec.ExampleMethod("value"));

            // The following line causes a compiler error because ExampleMethod
            // takes only one argument.
            //Console.WriteLine(ec.ExampleMethod(10, 4));

            dynamic dynamic_ec = new ExampleClass();
            Console.WriteLine(dynamic_ec.ExampleMethod(10));

            // Because dynamic_ec is dynamic, the following call to ExampleMethod
            // with two arguments does not produce an error at compile time.
            // However, it does cause a run-time error.
            //Console.WriteLine(dynamic_ec.ExampleMethod(10, 4));
        }
    }

    class ExampleClass
    {
        static dynamic _field;
        dynamic Prop { get; set; }

        public dynamic ExampleMethod(dynamic d)
        {
            dynamic local = "Local variable";
            int two = 2;

            if (d is int)
            {
                return local;
            }
            else
            {
                return two;
            }
        }
    }
}
// Results:
// Local variable
// 2
// Local variable

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格的下列幾節:

另請參閱