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

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

C# 语言参考记录了最近发布的 C# 语言版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。

本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。

小窍门

若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。

对象类型

object 类型是 System.Object 在 .NET 中的别名。 在 C# 的统一类型系统中,所有类型(预定义类型、用户定义类型、引用类型和值类型)都是直接或间接从 System.Object 继承的。 将任何类型的值(但见 ref structref 结构)分配给类型的object变量。 可以将文本 null 分配给任何 object 变量作为其默认值。 将值类型变量 object转换为时,将 装箱该值。 将类型的变量转换为值类型 object 时,将 取消装箱值。 有关详细信息,请参阅装箱和取消装箱。

字符串类型

string 类型表示零个或多个 Unicode 字符的序列。 在 .NET 中, string 是 .NET 的 System.String别名。

虽然 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 并采用三种形式:原始、带引号和逐字。

原始字符串文本 包含任意文本,无需转义序列。 字符串字面量可以包括空格和新行、嵌入引号以及其他特殊字符。 原始字符串字面量用至少三个双引号 (""") 括起来:

"""
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
    }
    """;

小窍门

Visual Studio 和 C# 开发工具包在原始字符串文本包含 JSON 数据或正则表达式时提供一些验证和语法突出显示。

这些工具分析文本。 如果工具确信文本表示 JSON 或正则表达式,编辑器将提供语法着色。

可以通过在声明上方添加指示格式的注释来改善该体验:

  • // lang=json 指示原始字符串文本表示 JSON 数据。
  • // lang=regex 指示原始字符串文本表示正则表达式。

当原始字符串文本用作参数时,参数使用该 System.Diagnostics.CodeAnalysis.StringSyntaxAttribute 参数来指示格式,这些工具将验证某些格式类型的原始字符串文本。 支持 JSON 和正则表达式。

对于某些格式,注释或属性启用代码建议,为基于格式的字符串文本提供修补程序。

如果任何文本行扩展到右引号序列的左侧,编译器将返回错误。 左引号和右引号序列可以位于同一行上,前提是字符串字面量既不能以引号字符开头,也不能以引号字符结尾:

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 协议和其他重要库的标准。 可以将后缀添加到 u8 字符串文本,以指定 UTF-8 编码。 编译器将 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.Action 和 System.Func 类型为许多常见委托提供泛型定义。 可能不需要定义新的自定义委托类型。 相反,可以创建提供的泛型类型的实例化。

它是 delegate 一种内置引用类型,可用于封装命名方法或匿名方法。 委托类似于 C++ 中的函数指针;但是,委托是类型安全和可靠的。 有关委托的应用,请参阅委托和泛型委托。 委托是事件的基础。 通过将委托与命名方法或匿名方法相关联来实例化委托。

必须使用具有兼容返回类型和输入参数的方法或 lambda 表达式实例化委托。 有关方法签名中允许的差异程度的详细信息,请参阅委托中的变体。 若要与匿名方法一起使用,请声明委托和要一起关联的代码。

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

Action<string> stringAction = str => {};
Action<object> objectAction = obj => {};

// Valid due to implicit reference conversion of
// objectAction to Action<string>, but might 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 类型简化了对 COM API(例如 Office Automation API)、动态 API(例如 IronPython 库)和 HTML 文档对象模型 (DOM) 的访问。

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

以下示例将类型的 dynamic 变量与类型 object变量形成对比。 若要在编译时验证每个变量的类型,请将鼠标指针放在 dyn 语句中的 obj 或 WriteLine 上。 请将下面的代码复制到可以使用 IntelliSense 的编辑器中。 IntelliSense 对 显示“dynamic”dyn,对 显示“object”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 语句显示 dyn 和 obj 的运行时类型。 此时,两者的类型均为整数。 将生成以下输出:

System.Int32
System.Int32

若要查看编译时 dyn 与 obj 之间的区别,请在前面示例的声明和 WriteLine 语句之间添加下列两行。

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

尝试在表达式 obj + 3 中添加整数和对象时,将报告编译器错误。 但是,对于 dyn + 3,不会报告任何错误。 在编译时不会检查包含 dyn 的表达式,原因是 dyn 的类型为 dynamic。

下面的示例在多个声明中使用 dynamic。 Main 方法也将编译时类型检查与运行时类型检查进行了对比。

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# 语言规范的以下部分:

另请参阅