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

Mik*_*ike 58 c# struct immutability

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

public struct Pair
{
    public readonly int x;
    public readonly int y;

    // Constructor and stuff
}
Run Code Online (Sandbox Code Playgroud)

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

在这个例子中,我使用了int.如果我使用了一个类,但该类也是不可变的,就像这样呢?这应该也可以正常工作,对吧?

public struct Pair
{
    public readonly (immutableClass) x;
    public readonly (immutableClass) y;

    // Constructor and stuff
}
Run Code Online (Sandbox Code Playgroud)

(旁白:据我所知,使用性能更加普及,并允许改变,但这种结构的目的是从字面上只存储两个值我在永恒的问题只是有兴趣在这里.)

Eri*_*ert 114

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

使所有字段只读是一种很好的方法来帮助(1)记录结构是不可变的,以及(2)防止意外突变.

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

例如,让我们采取你的结构:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

是否有任何事情可能发生在"此处发生的事情"导致调试断言被违反?当然.

    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);
Run Code Online (Sandbox Code Playgroud)

现在会发生什么?断言被违反了!"this"和"p"指的是相同的存储位置.存储位置发生了变异,因此"this"的内容发生了变异,因为它们是相同的.结构不能强制执行x和y的只读,因为结构不拥有存储; 存储是一个局部变量,可以根据需要随意变异.

您不能依赖于从未观察到结构中的只读字段发生变化的不变量; 您唯一可以依赖的是您无法编写直接更改它的代码.但是有了这样一个偷偷摸摸的工作,你可以间接改变它你想要的一切.

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

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

  • 令人惊讶的是,你展示了多少让我思考的边缘案例,"谁甚至会做*那个?" (19认同)
  • @Joel:假设我们想出了一种乘法对的方法,但它有时会失败.因此,您将"bool MultiplyBy(对x,对结果)"作为将"this"乘以x的方法,将成功或失败作为bool返回,并将结果写入别名变量.现在你有一对,你想要将它平方并用方块替换前一个值,所以你说"myPair.MultiplyBy(myPair,out myPair)".繁荣,你已经陷入了这个可怕的陷阱.沿途的每一步都非常合理,但它们加起来很可怕. (8认同)
  • 我发现当我调试其他人的代码时,你可以用`StructLayout.Explicit`做同样的事情. (4认同)
  • @Mike:*变量可以改变*.这就是为什么他们被称为"变量".如果你想要的是确保特定的*变量*永远不会改变,那就做一个只读.请记住,结构总是从其他人那里借用一个变量; 如果您希望结构的字段永远不会更改,请将它们只读并确保结构从其中借用存储的变量也是不变的.当然,我在这里描绘的问题在实践中并不经常出现. (2认同)
  • @BillAskaga:我同意人们常常被价值观和变数之间的差异所困惑,但我不明白这个问题应该归咎于这个答案; 问题似乎更为普遍和普遍.我想你错过了我的回答.我并不是说*值*是突变的,正如你正确指出的那样,值只是值; 我指出*即使在构造函数*之外,也可以观察到只读字段的内容随时间变化,这是违反直觉的. (2认同)
  • @EricLippert:我认为你应该更清楚地解释*x/y*字段确实没有改变,但是底层的*this*引用会导致*x/y*字段*看似*改变.所以主要问题是C#不允许我们只读取*myPair*字段 - 从而阻止我们首先重新分配它.当Pair是一个类时,相同代码表现不同的事实当然是非常违反直觉的. (2认同)

The*_*tor 8

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

public readonly struct Pair
{
    public int x;
    public int y;

    // Constructor and stuff
}
Run Code Online (Sandbox Code Playgroud)

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

正如Eric Lippert 的回答中所指出,这并不能阻止结构本身完全重新分配,从而提供其字段从您下方改变的效果。可以使用按值传递或使用新的in参数修饰符来帮助防止这种情况:

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


Mr4*_*r47 5

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