对不可变结构使用公共只读字段是否有效

本文关键字:读字段 是否 有效 不可变 结构 | 更新日期: 2023-09-27 17:57:05

这是声明不可变结构的正确方法吗?

public struct Pair
{
    public readonly int x;
    public readonly int y;
    // Constructor and stuff
}

我想不出为什么这会遇到问题,但我只是想问一下。

在这个例子中,我使用了整数。如果我改用一个类,但该类也是不可变的,就像这样,该怎么办?那应该也没问题,对吧?

public struct Pair
{
    public readonly (immutableClass) x;
    public readonly (immutableClass) y;
    // Constructor and stuff
}

(旁白:我知道使用属性更通用并允许更改,但这个结构实际上只是为了存储两个值。我只是对这里的不变性问题感兴趣。

对不可变结构使用公共只读字段是否有效

如果要使用结构,最佳做法是使它们不可变。

将所有字段设为只读是帮助 (1) 记录结构不可变和 (2) 防止意外突变的好方法。

然而,有一个皱纹,实际上在一个奇怪的巧合中,我计划在下周写博客。也就是说:结构字段上的只读是一个谎言。人们期望只读字段不能更改,但它当然可以。结构字段上的"只读"是在其帐户中没有钱的情况下写支票的声明。结构不拥有它的存储,正是这个存储可以变异。

例如,让我们以您的结构:

public struct Pair
{
    public readonly int x;
    public readonly int y;
    public Pair(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    public void M(ref Pair p)
    {
        int oldX = x;
        int oldY = y;
        // Something happens here
        Debug.Assert(x == oldX);
        Debug.Assert(y == oldY);
    }
}

在"这里发生了一些事情"时,是否会发生导致违反调试断言的事情? 确定。

    public void M(ref Pair p)
    {
        int oldX = this.x;
        int oldY = this.y;
        p = new Pair(0, 0);
        Debug.Assert(this.x == oldX);
        Debug.Assert(this.y == oldY);
    }
...
    Pair myPair = new Pair(10, 20);
    myPair.M(ref myPair);

现在会发生什么? 违反了断言!"this"和"p"指的是相同的存储位置。存储位置发生了变化,因此"this"的内容发生了变化,因为它们是同一件事。 该结构无法强制实施 x 和 y 的只读性,因为该结构不拥有存储;存储是一个局部变量,可以随心所欲地自由变异。

您不能

依赖于结构中的只读字段永远不会被观察到更改的不变性;您唯一可以依赖的是,您不能编写直接更改它的代码。但是通过像这样的小偷摸摸的工作,你可以间接地改变它。

另请参阅Joe Duffy关于此问题的优秀博客文章:

http://joeduffyblog.com/2010/07/01/when-is-a-readonly-field-not-readonly/

从 C# 7.2 开始,您现在可以将整个结构声明为不可变:

public readonly struct Pair
{
    public int x;
    public int y;
    // Constructor and stuff
}

这将具有与将所有字段标记为readonly相同的效果,并且还将向编译器本身记录结构是不可变的。这将通过减少编译器创建的防御性副本的数量来提高使用结构的区域的性能。

正如 Eric Lippert 的回答中所指出的,这并不妨碍结构本身被完全重新分配,从而提供其场从你身下改变的效果。按值传递或使用新的 in 参数修饰符可用于帮助防止出现这种情况:

public void DoSomething(in Pair p) {
    p.x = 0; // illegal
    p = new Pair(0, 0); // also illegal
}

这将使它确实不可变。我想你最好添加一个构造函数。
如果它的所有成员也是不可变的,这将使它完全不可变。这些可以是类或简单值。

编译器

将禁止分配给readonly字段以及只读属性。

我建议主要出于公共接口原因和数据绑定(不适用于字段)使用只读属性。如果这是我的项目,如果结构/类是公共的,我会要求这样做。如果它是程序集的内部属性或类的私有属性,我可以先忽略它,稍后将它们重构为只读属性。