结构类型(C# 参考)
结构类型(“structure type”或“struct type”)是一种可封装数据和相关功能的值类型 。 使用 struct
关键字定义结构类型:
public struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
public override string ToString() => $"({X}, {Y})";
}
ref struct
类型和 readonly ref struct
类型在 ref 结构类型一文中有所介绍。
结构类型具有值语义 。 也就是说,结构类型的变量包含类型的实例。 默认情况下,在分配中,通过将参数传递给方法并返回方法结果来复制变量值。 对于结构类型变量,将复制该类型的实例。 有关更多信息,请参阅值类型。
通常,可以使用结构类型来设计以数据为中心的较小类型,这些类型只有很少的行为或没有行为。 例如,.NET 使用结构类型来表示数字(整数和实数)、布尔值、Unicode 字符以及时间实例。 如果侧重于类型的行为,请考虑定义一个类。 类类型具有引用语义 。 也就是说,类类型的变量包含的是对类型的实例的引用,而不是实例本身。
由于结构类型具有值语义,因此建议定义不可变的结构类型。
readonly
结构
可以使用 readonly
修饰符来声明结构类型为不可变。 readonly
结构的所有数据成员都必须是只读的,如下所示:
- 任何字段声明都必须具有
readonly
修饰符 - 任何属性(包括自动实现的属性)都必须是只读的。 在 C# 9.0 和更高版本中,属性可以具有
init
访问器。
这样可以保证 readonly
结构的成员不会修改该结构的状态。 这意味着除构造函数外的其他实例成员是隐式 readonly
。
注意
在 readonly
结构中,可变引用类型的数据成员仍可改变其自身的状态。 例如,不能替换 List<T> 实例,但可以向其中添加新元素。
下面的代码使用 init-only 属性资源库定义 readonly
结构,此内容在 C# 9.0 及更高版本中提供:
public readonly struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; init; }
public double Y { get; init; }
public override string ToString() => $"({X}, {Y})";
}
readonly
实例成员
还可以使用 readonly
修饰符来声明实例成员不会修改结构的状态。 如果不能将整个结构类型声明为 readonly
,可使用 readonly
修饰符标记不会修改结构状态的实例成员。
在 readonly
实例成员内,不能分配到结构的实例字段。 但是,readonly
成员可以调用非 readonly
成员。 在这种情况下,编译器将创建结构实例的副本,并调用该副本上的非 readonly
成员。 因此,不会修改原始结构实例。
通常,将 readonly
修饰符应用于以下类型的实例成员:
方法:
public readonly double Sum() { return X + Y; }
还可以将
readonly
修饰符应用于可替代在 System.Object 中声明的方法的方法:public readonly override string ToString() => $"({X}, {Y})";
属性和索引器:
private int counter; public int Counter { readonly get => counter; set => counter = value; }
如果需要将
readonly
修饰符应用于属性或索引器的两个访问器,请在属性或索引器的声明中应用它。注意
编译器会将自动实现的属性的
get
访问器声明为readonly
,而不管属性声明中是否存在readonly
修饰符。在 C# 9.0 和更高版本中,可以将
readonly
修饰符应用于具有init
访问器的属性或索引器:public readonly double X { get; init; }
可以将 readonly
修饰符应用于结构类型的静态字段,但不能应用于任何其他静态成员,例如属性或方法。
编译器可以使用 readonly
修饰符进行性能优化。 有关详细信息,请参阅编写安全有效的 C# 代码。
非破坏性变化
从 C# 10 开始,可以使用 with
表达式来生成修改了指定属性和字段的结构类型实例的副本。 使用对象初始值设定项语法来指定要修改的成员及其新值,如以下示例所示:
public readonly struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; init; }
public double Y { get; init; }
public override string ToString() => $"({X}, {Y})";
}
public static void Main()
{
var p1 = new Coords(0, 0);
Console.WriteLine(p1); // output: (0, 0)
var p2 = p1 with { X = 3 };
Console.WriteLine(p2); // output: (3, 0)
var p3 = p1 with { X = 1, Y = 4 };
Console.WriteLine(p3); // output: (1, 4)
}
record
结构
从 C# 10 开始,可定义记录结构类型。 记录类型提供用于封装数据的内置功能。 可同时定义 record struct
和 readonly record struct
类型。 记录结构不能是 ref struct
。 有关详细信息和示例,请参阅记录。
结构初始化和默认值
struct
类型的变量直接包含该 struct
类型的数据。 这会让未初始化的 struct
(具有其默认值)和已初始化的 struct
(通过构造值来存储一组值)之间存在区别。 例如,考虑下面的代码:
public readonly struct Measurement
{
public Measurement()
{
Value = double.NaN;
Description = "Undefined";
}
public Measurement(double value, string description)
{
Value = value;
Description = description;
}
public double Value { get; init; }
public string Description { get; init; }
public override string ToString() => $"{Value} ({Description})";
}
public static void Main()
{
var m1 = new Measurement();
Console.WriteLine(m1); // output: NaN (Undefined)
var m2 = default(Measurement);
Console.WriteLine(m2); // output: 0 ()
var ms = new Measurement[2];
Console.WriteLine(string.Join(", ", ms)); // output: 0 (), 0 ()
}
如前面的示例所示,默认值表达式忽略了无参数构造函数,并生成了结构类型的默认值。 结构类型数组实例化还忽略无参数构造函数并生成使用结构类型的默认值填充的数组。
你将看到默认值的最常见情况是在数组中或内部存储包含变量块的其他集合中。 以下示例创建了一个由 30 个 TemperatureRange
结构组成的数组,每个结构都具有默认值:
// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];
结构的所有成员字段在创建时必须进行明确指定,因为 struct
类型直接存储其数据。 结构的 default
值已将所有字段明确指定为 0。 调用构造函数时,必须明确指定所有字段。 可以使用以下机制初始化字段:
- 可以将字段初始化表达式添加到任何字段或自动实现的属性。
- 可以在构造函数主体中初始化任何字段或自动属性。
从 C# 11 开始,如果你没有初始化结构中的所有字段,编译器会将代码添加到将这些字段初始化为默认值的构造函数中。 编译器执行其常用的明确指定分析。 在指定之前访问的任何字段,或者当构造函数完成执行时未明确指定的字段,会在构造函数主体执行之前被指定其默认值。 如果在指定所有字段之前访问 this
,则结构会在构造函数主体执行之前初始化为默认值。
public readonly struct Measurement
{
public Measurement(double value)
{
Value = value;
}
public Measurement(double value, string description)
{
Value = value;
Description = description;
}
public Measurement(string description)
{
Description = description;
}
public double Value { get; init; }
public string Description { get; init; } = "Ordinary measurement";
public override string ToString() => $"{Value} ({Description})";
}
public static void Main()
{
var m1 = new Measurement(5);
Console.WriteLine(m1); // output: 5 (Ordinary measurement)
var m2 = new Measurement();
Console.WriteLine(m2); // output: 0 ()
var m3 = default(Measurement);
Console.WriteLine(m3); // output: 0 ()
}
每个 struct
都具有一个 public
无参数构造函数。 如果要编写无参数构造函数,它必须是公共构造函数。 如果结构声明了任何字段初始值设定项,就必须显式声明一个构造函数。 该构造函数不必是无参数的。 如果结构声明了字段初始值设定项,但没有构造函数,编译器将报告错误。 任何显式声明的构造函数(有参数或无参数)都会执行该结构的所有字段初始值设定项。 没有字段初始值设定项或构造函数的赋值的所有字段均设置为默认值。 有关详细信息,请参阅无参数结构构造函数功能建议说明。
如果结构类型的所有实例字段都是可访问的,则还可以在不使用 new
运算符的情况下对其进行实例化。 在这种情况下,在首次使用实例之前必须初始化所有实例字段。 下面的示例演示如何执行此操作:
public static class StructWithoutNew
{
public struct Coords
{
public double x;
public double y;
}
public static void Main()
{
Coords p;
p.x = 3;
p.y = 4;
Console.WriteLine($"({p.x}, {p.y})"); // output: (3, 4)
}
}
在处理内置值类型的情况下,请使用相应的文本来指定类型的值。
结构类型的设计限制
结构具有类类型的大部分功能。 存在一些异常情况,在较新版本中也删除了一些异常:
- 结构类型不能从其他类或结构类型继承,也不能作为类的基础类型。 但是,结构类型可以实现接口。
- 不能在结构类型中声明终结器。
- 在 C# 11 之前,结构类型的构造函数必须初始化该类型的所有实例字段。
- 在 C# 10 之前,不能声明无参数构造函数。
- 在 C# 10 之前,不能在声明实例字段或属性时对它们进行初始化。
按引用传递结构类型变量
将结构类型变量作为参数传递给方法或从方法返回结构类型值时,将复制结构类型的整个实例。 通过值传递可能会影响高性能方案中涉及大型结构类型的代码的性能。 通过按引用传递结构类型变量,可以避免值复制操作。 使用 ref
、out
或 in
方法参数修饰符,指示必须按引用传递参数。 使用 ref 返回值按引用返回方法结果。 有关详细信息,请参阅编写安全有效的 C# 代码。
struct 约束
你还可在 struct
约束中使用 struct
关键字,来指定类型参数为不可为 null 的值类型。 结构类型和枚举类型都满足 struct
约束。
转换
对于任何结构类型(ref struct
类型除外),都存在与 System.ValueType 和 System.Object 类型之间的装箱和取消装箱相互转换。 还存在结构类型和它所实现的任何接口之间的装箱和取消装箱转换。
C# 语言规范
有关 struct
功能的详细信息,请参阅以下功能建议说明: