在 C# 中创建类型

本教程教你如何在 C# 中创建类型。 你将编写少量的代码,然后编译并运行这些代码。 本教程包含一系列探索 C# 中不同类型的课程。 这些课程介绍了 C# 语言的基础知识。

小提示

当代码片段块包含“运行”按钮时,该按钮会打开交互式窗口,或替换交互式窗口中的现有代码。 如果代码段不包含“运行”按钮,则可以复制代码并将其添加到当前交互式窗口中。

前面的教程使用的是文本和数字。 字符串和数字是简单类型:它们各自存储一个值。 随着程序规模的扩大,需要使用更为复杂的数据结构。 在需要具有更多字段、属性或行为的数据结构时,C# 提供了可以定义的不同类型。 现在开始探讨这些类型。

元组

元组是具有固定长度的有序数值序列。 元组的每个元素都有一个类型和一个可选名称。 以下代码声明了一个表示二维点的元组。 选择“运行”按钮,将以下代码粘贴到交互式窗口并运行。

var pt = (X: 1, Y: 2);

var slope = (double)pt.Y / (double)pt.X;
Console.WriteLine($"A line from the origin to the point {pt} has a slope of {slope}.");

小提示

在探索 C#(或任何编程语言)的过程中,可能会在编写代码时犯错。 编译器会发现这些错误并向你报告。 如果输出中包含错误消息,请仔细查看示例代码和交互式窗口中的代码,以了解应如何纠正。 该练习有助于学习 C# 代码的结构。

可以重新分配元组中的任何成员。 在交互式窗口中现有代码后添加以下代码。 再次按“运行”以查看结果。

pt.X = pt.X + 5;
Console.WriteLine($"The point is now at {pt}.");

还可以使用 with 表达式来创建一个新元组,它是原始元组的修改副本。 在交互式窗口中的代码后添加以下代码,然后按“运行”查看结果:

var pt2 = pt with { Y = 10 };
Console.WriteLine($"The point 'pt2' is at {pt2}.");

元组 pt2 包含 ptX 值 (6),而 pt2.Y 是 10。

元组是结构类型。 换言之,元组类型没有 stringint 这样的名称。 元组类型由成员数(称为 arity)和这些成员的类型定义。 成员姓名是为了方便起见。 即使成员具有不同的名称,也可以将元组赋值给具有相同算式和类型的元组。 可以在交互式窗口中已编写的代码后添加以下代码并尝试:

var subscript = (A: 0, B: 0);
subscript = pt;
Console.WriteLine(subscript);

变量 subscript 有两个成员,两者均为整数。 subscriptpt 表示同一元组类型的实例:包含 2 个 int 成员的元组。

元组很容易创建:可以声明多个用括号括起来的成员。 以下所有元组都声明了不同的元数和成员类型。 添加以下代码以创建新的元组类型:

var namedData = (Name: "Morning observation", Temp: 17, Wind: 4);
var person = (FirstName: "", LastName: "");
var order = (Product: "guitar picks", style: "triangle", quantity: 500, UnitPrice: 0.10m);

元组很容易创建,但功能有限。 元组类型没有名称,因此无法向值集传达含义。 元组类型不能添加行为。 当类型定义了行为时,C# 还可以创建其他类型。

创建记录类型

如果需要在同一结构中使用多个值,元组是个不错的选择。 它们很轻量,并且可以在使用时进行声明。 在程序运行过程中,可能会发现整个代码都在使用相同的元组类型。 如果应用确实在二维图形空间中运行,那么表示点的元组可能很常见。 找到这些值后,就可以声明一个 record 类型来存储这些值,并提供更多的功能。 下面的代码示例使用 Main 方法表示程序的入口点。 这样,就可以在代码的入口点之前声明一个 record 类型。 按以下代码上的“运行”按钮,用以下代码替换现有样本。

警告

不要复制和粘贴。 要运行以下示例,必须重置交互式窗口。 如果出错,窗口就会挂起,需要刷新页面才能继续。

以下代码声明并使用 record 类型来表示 Point,然后在 Main 方法中使用该 Point 结构:

public record Point(int X, int Y);

public static void Main()
{
    Point pt = new Point(1, 1);
    var pt2 = pt with { Y = 10 };
    Console.WriteLine($"The two points are {pt} and {pt2}");
}

record 声明是 Point 类型的一行代码,它将 XY 值存储在只读属性中。 只要使用该类型,就必须使用 Point 名称。 正确命名的类型(如 Point)会提供有关如何使用该类型的信息。 Main 方法介绍了如何使用 with 表达式来创建一个新点,该点是现有点的修改副本。 pt2 = pt with { Y = 10 } 行中写道:“除了 Y 被赋值为 10 之外,pt2 的值与 pt 相同”。可以在一个 with 表达式中添加任意数量要更改的属性。

与所有 C# 语句一样,前面的 record 声明是一行以 ; 结尾的代码。 可以通过声明成员record 类型添加行为。 记录成员可以是一个函数,也可以是多个数据元素。 一个类型的成员在类型声明中,位于 {} 字符之间。 用以下代码替换你所做的记录声明:

public record Point(int X, int Y)
{
    public double Slope() => (double)Y / (double)X;
}

然后,在 Main 方法中包含 with 表达式的行之后添加以下代码:

double slope = pt.Slope();
Console.WriteLine($"The slope of {pt} is {slope}");

你为代表 XY 值的元组添加了格式。 你让它成为一个 record,定义了一个命名的类型,并包含了一个计算斜率的成员。 record 类型是 record class 的简写: 包含额外行为的 class 类型。 也可以修改 Point 类型,使其成为 record struct

public record struct Point(int X, int Y)
{
    public double Slope() => (double) Y / (double) X;
}

record struct 是一种 struct 类型,包含添加到所有 record 类型中的额外行为。

结构、类和接口类型

C# 中的所有命名类型都是 classstruct 类型。 class引用类型struct 是一个值类型。 值类型的变量在内存中内联存储实例的内容。 换言之,record struct Point 存储了两个整数:XY。 引用类型的变量存储指向实例存储空间的引用或指针。 换言之,record class Point 存储的是对内存块的引用,该内存块保存了 XY 的值。

实际上,这意味着值类型在分配时会被复制,但类实例的副本就是引用的副本。 复制的引用指向同一个点的实例,XY 的存储空间相同。

record 修改器指示编译器为你编写多个成员。 可以在基础知识部分的记录类型一文中了解更多信息。

在声明一个 record 类型时,你就声明了自己的类型应该使用一组默认行为来进行等价比较、赋值和复制该类型的实例。 当存储相关数据是你的类型的主要责任时,记录是最好的选择。 在添加更多行为时,请考虑使用 structclass 类型,而不使用 record 修饰符。

当需要更复杂的行为时,可以使用 struct 类型作为值类型,但其主要职责是存储值。 可以使用 class 类型来使用面向对象的习语,如封装、继承和多态性。

还可以定义 interface 类型来声明不同类型必须实现的行为合约。 structclass 类型都可以实现接口。

你通常会在大型程序和程序库中使用所有这些类型。 在安装 .NET SDK 后,可以使用基础知识部分的教程来探索这些类型。

你已完成“在 C# 中创建类型”交互式教程。 可选择分支和循环链接开始下一个交互式教程,或者可访问 .NET 站点下载 .NET SDK,在计算机上创建项目,并继续编码。 请通过“后续步骤”部分返回到这些教程。

可以在以下文章中了解有关 C# 中类型的详细信息: