如何使一个结构体不可变?

29

在整个 Stack Overflow 和互联网上,我看到保持结构体不可变是一种好的设计原则。不幸的是,我从未见过任何实现可以使这些结构体真正不可变。

假设一个结构体内部没有任何引用类型,我该如何实际使一个结构体不可变呢?也就是说,我该如何防止其所有基本字段的变异(也许通过编译时或运行时异常)?

我写了一个简单的测试试图使结构体不可变,但甚至使用 System.ComponentModel.ImmutableObjectAttribute 也不起作用:


```C# // 程序代码 ```
class Program
{
    static void Main(string[] args)
    {
        ImmutableStruct immStruct1 = new ImmutableStruct();
        Console.WriteLine(immStruct1); //Before mutation.

        immStruct1.field1 = 1;
        immStruct1.field2 = "Hello";
        immStruct1.field3 = new object();
        Console.WriteLine(immStruct1); //After 1st mutation.

        immStruct1.field1 = 2;
        immStruct1.field2 = "World";
        immStruct1.field3 = new object();
        Console.WriteLine(immStruct1); //After 2nd mutation.

        Console.ReadKey();
    }
}

[ImmutableObject(true)]
struct ImmutableStruct
{
    public int field1;
    public string field2;
    public object field3;

    public override string ToString()
    {
        string field3String = "null";
        if (field3 != null)
        {
            field3String = field3.GetHashCode().ToString();
        }
        return String.Format("Field1: {0}, Field2: {1}, Field3: {2}", field1, field2, field3String);
    }
}

1
有点离题,但你需要使用结构体而不是只使用类吗?在多年的LOB编程中,我没有发现结构体比类更可取的用例。 - StingyJack
将字段标记为只读?此外,您是否看到过这个:http://stackoverflow.com/questions/29974301/should-a-c-sharp-struct-have-only-read-only-properties - Pawel
所考虑的结构类似于C++中的位域结构体。该结构被实例化,我的愿望是该结构永远不会被改变——这是一个承诺,我没有意外修改数据。 - Nicholas Miller
2个回答

36

将字段设置为private readonly,并在构造函数中传递初始值

public struct ImmutableStruct
{
    private readonly int _field1;
    private readonly string _field2;
    private readonly object _field3;

    public ImmutableStruct(int f1, string f2, object f3)
    {
        _field1 = f1;
        _field2 = f2;
        _field3 = f3;
    }

    public int Field1 { get { return _field1; } }
    public string Field2 { get { return _field2; } }
    public object Field3 { get { return _field3; } }
}

从C#6.0(Visual Studio 2015)开始,您可以使用只读属性

public struct ImmutableStruct
{
    public ImmutableStruct(int f1, string f2, object f3)
    {
        Field1 = f1;
        Field2 = f2;
        Field3 = f3;
    }

    public int Field1 { get; }
    public string Field2 { get; }
    public object Field3 { get; }
}
请注意,只读字段和仅具有getter属性的属性可以在构造函数中初始化,或者在类中也可以使用字段或属性初始化程序进行初始化 public int Field1 { get; } = 7;
不能保证结构体上运行构造函数。例如,如果您有一个结构体数组,则必须为每个数组元素显式调用初始化程序。对于引用类型的数组,所有元素都首先初始化为null,这使得您必须在每个元素上调用new。但是很容易忘记对于结构体等值类型。
var immutables = new ImmutableStruct[10];
immutables[0] = new ImmutableStruct(5, "hello", new Person());
immutables[1] = new ImmutableStruct(6, "world", new Student());
...

从C# 7.2开始,您可以使用只读结构体


从C# 9.0开始,还有另一种选择: 初始化只读属性。 可以在构造函数和字段或属性初始化程序中初始化只读字段和只有get的自动实现属性,但不能在对象初始化程序中进行。

这是引入init-only属性的动机。它们将set访问器替换为init访问器。这将扩展突变阶段,从实际对象创建到整个对象构建阶段,包括对象初始化程序和with表达式(也是C# 9.0的新功能)。

public string Name { get; init; }

使用方法:

var x = new ImmutableStruct { Name = "John" }; // Okay

x.Name = "Sue"; // Compiler error CS8852: Init-only property or indexer
                // 'ImmutableStruct.Name' can only be assigned in an object
                // initializer, or on 'this' or 'base' in an instance constructor
                // or an 'init' accessor.

C# 10.0(Visual Studio 2022)引入了记录结构体无参数结构体构造函数和字段初始化器。

readonly record struct Point(int X, int Y);

这将生成具有getinit访问器的XY属性。


3
由于Field3是一个引用类型,因此它仍被视为可变。为了保持实例的不可变性,在getter中必须返回对象的副本。 - Crowcoder
4
或者使被引用的对象本身成为不可变。String 是一个引用类型,但是它是不可变的。由于结构体代表值类型,因此具有对可变引用类型的引用可能也不是一个好主意。 - Olivier Jacot-Descombes
好的回答!它能够正常工作并且是最新的!顺便说一下,如果你有一个属性初始化器,它只能出现在类中。否则,你会收到错误信息:“不能在结构体中具有实例属性或字段初始化器” - Nicholas Miller
好的,这很有道理,因为初始化代码不能保证运行(例如在数组中)。 - Olivier Jacot-Descombes
字符串操作会返回新的字符串,而不是在原有字符串上进行修改。将Field3也设置为不可变是可以的,但如果您无法控制该类型呢?例如 SqlConnection? - Crowcoder
2
我认为你是否将结构体视为不可变的取决于你是否只关心对象的引用相等性,或者你也关心对象状态。对于你来说,知道结构体始终返回相同的连接对象可能已经足够了。 - Olivier Jacot-Descombes

3

保持您的不可变数据私有:

struct ImmutableStruct
{
    private int field1;
    private string field2;
    private object field3;

    public ImmutableStruct(int f1, string f2, object f3)
    {
        field1 = f1;
        field2 = f2;
        field3 = f3;
    }

    public int Field1 => field1;
    public string Field2 => field2;
    public object Field3 => field3;
}

或者更简洁一些:

struct ImmutableStruct
{
    public ImmutableStruct(int f1, string f2, object f3)
    {
        Field1 = f1;
        Field2 = f2;
        Field3 = f3;
    }

    public int Field1 { get; }
    public string Field2 { get; }
    public object Field3 { get; }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接