结构类型(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})";
}

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

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

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

readonly 结构

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

  • 任何字段声明都必须具有 readonly 修饰符
  • 任何属性(包括自动实现的属性)都必须是只读的。 在 C# 9.0 和更高版本中,属性可以具有 init 访问器

这样可以保证 readonly 结构的成员不会修改该结构的状态。 在 C# 8.0 及更高版本中,这意味着除构造函数外的其他实例成员是隐式 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 实例成员

从 C#8.0 开始,还可以使用 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 表达式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)
}

结构类型的设计限制

设计结构类型时,具有与类型相同的功能,但有以下例外:

  • 不能声明无参数构造函数。 每个结构类型都已经提供了一个隐式无参数构造函数,该构造函数生成类型的默认值

    备注

    从 C# 10 开始,可以在结构类型中声明无参数构造函数。 有关详细信息,请参阅无参数构造函数和字段初始值设定项部分。

  • 不能在声明实例字段或属性时对它们进行初始化。 但是,可以在其声明中初始化静态常量字段或静态属性。

    备注

    从 C# 10 开始,可以在声明实例字段或属性时对它们进行初始化。 有关详细信息,请参阅无参数构造函数和字段初始值设定项部分。

  • 结构类型的构造函数必须初始化该类型的所有实例字段。

  • 结构类型不能从其他类或结构类型继承,也不能作为类的基础类型。 但是,结构类型可以实现接口

  • 不能在结构类型中声明终结器

无参数构造函数和字段初始值设定项

从 C# 10 开始,可以在结构类型中声明无参数实例构造函数,如以下示例所示:

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 ()
}

如前面的示例所示,默认值表达式忽略无参数构造函数并生成结构类型的默认值,该值是通过如下设置生成的值:将所有值类型的字段设置为其默认值(0 位模式),将所有引用类型的字段设置为 null。 结构类型数组实例化还忽略无参数构造函数并生成使用结构类型的默认值填充的数组。

从 C# 10 开始,还可以在声明实例字段或属性时对它们进行初始化,如以下示例所示:

public readonly struct Measurement
{
    public Measurement(double value)
    {
        Value = value;
    }

    public Measurement(double value, string description)
    {
        Value = value;
        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 ()
}

如果未显式声明无参数构造函数,则结构类型会提供无参数构造函数,其行为如下所示:

  • 如果结构类型具有显式实例构造函数或没有字段初始值设定项,则隐式无参数构造函数将生成结构类型的默认值,而不管字段初始值设定项为何,如前面的示例所示。

  • 如果结构类型没有显式实例构造函数,但具有字段初始值设定项,则编译器将合成一个用来执行指定字段初始化的公共无参数构造函数,如以下示例所示:

    public struct Coords
    {
        public double X = double.NaN;
        public double Y = double.NaN;
    
        public override string ToString() => $"({X}, {Y})";
    }
    
    public static void Main()
    {
        var p1 = new Coords();
        Console.WriteLine(p1);  // output: (NaN, NaN)
    
        var p2 = default(Coords);
        Console.WriteLine(p2);  // output: (0, 0)
    
        var ps = new Coords[3];
        Console.WriteLine(string.Join(", ", ps));  // output: (0, 0), (0, 0), (0, 0)
    }
    

如前面的示例所示,默认值表达式和数组实例化忽略字段初始值设定项。

有关详细信息,请参阅无参数结构构造函数功能建议说明。

结构类型的实例化

在 C# 中,必须先初始化已声明的变量,然后才能使用该变量。 由于结构类型变量不能为 null(除非它是可为空的值类型的变量),因此,必须实例化相应类型的实例。 有多种方法可实现此目的。

通常,可使用 new 运算符调用适当的构造函数来实例化结构类型。 每个结构类型都至少有一个构造函数。 这是一个隐式无参数构造函数,用于生成类型的默认值。 还可以使用默认值表达式来生成类型的默认值。

如果结构类型的所有实例字段都是可访问的,则还可以在不使用 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)
    }
}

在处理内置值类型的情况下,请使用相应的文本来指定类型的值。

按引用传递结构类型变量

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

ref 结构

从 C# 7.2 开始,可以在结构类型的声明中使用 ref 修饰符。 ref 结构类型的实例在堆栈上分配,并且不能转义到托管堆。 为了确保这一点,编译器将 ref 结构类型的使用限制如下:

  • ref 结构不能是数组的元素类型。
  • ref 结构不能是类或非 ref 结构的字段的声明类型。
  • ref 结构不能实现接口。
  • ref 结构不能被装箱为 System.ValueTypeSystem.Object
  • ref 结构不能是类型参数。
  • ref 结构变量不能由 lambda 表达式本地函数捕获。
  • ref 结构变量不能在 async 方法中使用。 但是,可以在同步方法中使用 ref 结构变量,例如,在返回 TaskTask<TResult> 的方法中。
  • ref 结构变量不能在迭代器中使用。

通常,如果需要一种同时包含 ref 结构类型的数据成员的类型,可以定义 ref 结构类型:

public ref struct CustomRef
{
    public bool IsValid;
    public Span<int> Inputs;
    public Span<int> Outputs;
}

若要将 ref 结构声明为 readonly,请在类型声明中组合使用 readonly 修饰符和 ref 修饰符(readonly 修饰符必须位于 ref 修饰符之前):

public readonly ref struct ConversionRequest
{
    public ConversionRequest(double rate, ReadOnlySpan<double> values)
    {
        Rate = rate;
        Values = values;
    }

    public double Rate { get; }
    public ReadOnlySpan<double> Values { get; }
}

在 .NET 中,ref 结构的示例分别是 System.Span<T>System.ReadOnlySpan<T>

struct 约束

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

转换

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

C# 语言规范

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

有关 C#7.2 及更高版本中引入的功能的更多信息,请参见以下功能建议说明:

另请参阅