结构类型(C# 参考)

结构类型(或 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 structreadonly ref struct 类型的信息,请参阅参考结构类型一文。

结构类型具有值语义。 也就是说,结构类型的变量包含类型的实例。 默认情况下,在分配中,通过将参数传递给方法并返回方法结果来复制变量值。 对于结构类型变量,将复制该类型的实例。 有关更多信息,请参阅值类型

通常,可以使用结构类型来设计以数据为中心的较小类型,这些类型只有很少的行为或没有行为。 例如,.NET 使用结构类型来表示数字(整数实数)、布尔值Unicode 字符以及时间实例。 如果侧重于类型的行为,请考虑定义一个。 类类型具有引用语义。 也就是说,类类型的变量包含的是对类型的实例的引用,而不是实例本身。

由于结构类型具有值语义,因此建议定义不可变的结构类型。

readonly 结构

可以使用 readonly 修饰符来声明结构类型为不可变。 readonly 结构的所有数据成员都必须是只读的,如下所示:

这样可以保证 readonly 结构的成员不会修改该结构的状态。 这意味着除构造函数外的其他实例成员是隐式 readonly

注意

readonly 结构中,可变引用类型的数据成员仍可改变其自身的状态。 例如,不能替换 List<T> 实例,但可以向其中添加新元素。

下面的代码使用 init-only 属性资源库定义 readonly 结构:

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修饰符。

    可以将 readonly 修饰符应用于具有 init 访问器的属性或索引器:

    public readonly double X { get; init; }
    

可以将 readonly 修饰符应用于结构类型的静态字段,但不能应用于任何其他静态成员,例如属性或方法。

编译器可以使用 readonly 修饰符进行性能优化。 有关详细信息,请参阅避免分配

非破坏性变化

从 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 structreadonly record struct 类型。 记录结构不能是 ref struct。 有关详细信息和示例,请参阅记录

内联数组

从 C# 12 开始,可以将内联数组声明为 struct 类型:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}

内联数组是包含相同类型的 N 个元素的连续块的结构。 它是一个安全代码,等效于仅在不安全代码中可用的固定缓冲区声明。 内联数组是具有以下特征的 struct

  • 它包含单个字段。
  • 结构未指定显式布局。

此外,编译器还会验证 System.Runtime.CompilerServices.InlineArrayAttribute 属性:

  • 必须大于零 (> 0)。
  • 目标类型必须是结构。

在大多数情况下,可以像访问数组一样访问内联数组,以读取和写入值。 此外,还可以使用范围索引运算符。

对内联数组的单个字段的类型有最小的限制。 它不能是指针类型:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithPointer
{
    private unsafe char* _pointerElement;    // CS9184
}

但它可以是任何引用类型,也可以是任何值类型:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithReferenceType
{
    private string _referenceElement;
}

几乎可以将内联数组与任何 C# 数据结构一起使用。

内联数组是一种高级语言功能。 它们适用于高性能方案,在这些方案中,内联的连续元素块比其他替代数据结构速度更快。 可以从功能规范中了解有关内联数组的详细信息

结构初始化和默认值

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 无参数构造函数。 如果要编写无参数构造函数,它必须是公共构造函数。 如果结构声明了任何字段初始值设定项,就必须显式声明一个构造函数。 该构造函数不必是无参数的。 如果结构声明了字段初始值设定项,但没有构造函数,编译器将报告错误。 任何显式声明的构造函数(有参数或无参数)都会执行该结构的所有字段初始值设定项。 没有字段初始值设定项或构造函数的赋值的所有字段均设置为默认值。 有关详细信息,请参阅无参数结构构造函数功能建议说明。

从 C# 12 开始,struct 类型可以将主构造函数定义为其声明的一部分。 主要构造函数为构造函数参数提供了简洁的语法,可在该结构的任何成员声明中的整个 struct 正文中使用。

如果结构类型的所有实例字段都是可访问的,则还可以在不使用 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 之前,结构类型的构造函数必须初始化该类型的所有实例字段。

按引用传递结构类型变量

将结构类型变量作为参数传递给方法或从方法返回结构类型值时,将复制结构类型的整个实例。 通过值传递可能会影响高性能方案中涉及大型结构类型的代码的性能。 通过按引用传递结构类型变量,可以避免值复制操作。 使用 refoutinref readonly 方法参数修饰符,指示必须按引用传递某个参数。 使用 ref 返回值按引用返回方法结果。 有关详细信息,请参阅避免分配

struct 约束

你还可在 struct 约束中使用 struct 关键字,来指定类型参数为不可为 null 的值类型。 结构类型和枚举类型都满足 struct 约束。

转换

对于任何结构类型(ref struct 类型除外),都存在与 System.ValueTypeSystem.Object 类型之间的装箱和取消装箱相互转换。 还存在结构类型和它所实现的任何接口之间的装箱和取消装箱转换。

C# 语言规范

有关详细信息,请参阅 C# 语言规范中的结构部分。

有关 struct 功能的详细信息,请参阅以下功能建议说明:

另请参阅