内置引用类型(C# 引用)

C# 有许多内置引用类型。 这些类型包含的关键字或运算符是 .NET 库中的类型的同义词。

对象类型

object 类型是 System.Object 在 .NET 中的别名。 在 C# 的统一类型系统中,所有类型(预定义类型、用户定义类型、引用类型和值类型)都是直接或间接从 System.Object 继承的。 可以将任何类型的值赋给 object 类型的变量。 可以使用文本 null 将任何 object 变量赋值给其默认值。 将值类型的变量转换为对象的过程称为装箱。 将 object 类型的变量转换为值类型的过程称为取消装箱。 有关详细信息,请参阅装箱和取消装箱

字符串类型

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 表达式实例化委托。 有关方法签名中允许的差异程度的详细信息,请参阅委托中的变体。 为了与匿名方法一起使用,委托和与之关联的代码必须一起声明。

当运行时涉及的委托类型因变体转换而不同时,委托组合或删除将失败,并出现运行时异常。 以下示例演示了失败的情况:

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;

从 C# 9 开始,可以声明使用类似语法的函数指针。 函数指针使用 calli 指令,而不是实例化委托类型和调用虚拟 Invoke 方法。

动态类型

dynamic 类型表示变量的使用和对其成员的引用绕过编译时类型检查。 改为在运行时解析这些操作。 dynamic 类型简化了对 COM API(例如 Office Automation API)、动态 API(例如 IronPython 库)和 HTML 文档对象模型 (DOM) 的访问。

在大多数情况下,dynamic 类型与 object 类型的行为类似。 具体而言,任何非 Null 表达式都可以转换为 dynamic 类型。 dynamic 类型与 object 的不同之处在于,编译器不会对包含类型 dynamic 的表达式的操作进行解析或类型检查。 编译器将有关该操作信息打包在一起,之后这些信息会用于在运行时评估操作。 在此过程中,dynamic 类型的变量会编译为 object 类型的变量。 因此,dynamic 类型只在编译时存在,在运行时则不存在。

下面的示例将 dynamic 类型的变量与 object 类型的变量进行对比。 若要在编译时验证每个变量的类型,请将鼠标指针放在 WriteLine 语句中的 dynobj 上。 请将下面的代码复制到可以使用 IntelliSense 的编辑器中。 IntelliSense 对 dyn 显示“dynamic”,对 obj 显示“object”

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 的表达式,原因是 dyn 的类型为 dynamic

下面的示例在多个声明中使用 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

另请参阅