【问题标题】:Does using public readonly fields for immutable structs work?对不可变结构使用公共只读字段是否有效?
【发布时间】:2018-07-31 04:42:52
【问题描述】:

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

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
}

(顺便说一句:我知道使用 Properties 更通用并且允许更改,但是这个结构实际上是为了存储两个值。我只是对这里的不变性问题感兴趣。)

【问题讨论】:

  • readonly properties/members 只能在构造函数中设置(最迟)。它们不能使用属性初始化语法来设置。
  • readonly 仅影响赋值运算符。它没有 C++ 的 const 关键字那么强的语义。

标签: c# struct immutability


【解决方案1】:

如果您要使用结构,最好使它们不可变。

将所有字段设为只读是一种很好的方式,有助于 (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/

【讨论】:

  • 令人惊讶的是,你展示了多少边缘案例让我想,“谁会这样做?”
  • 当我调试别人的代码时,我发现你可以用 StructLayout.Explicit 做同样的事情。
  • @Joel:假设我们想出了一种方法来乘以对,但它有时会失败。所以你有“bool MultiplyBy(Pair x, out Pair result)”作为一个方法,它将“this”乘以x,将成功或失败作为布尔值返回,并将结果写入别名变量。现在你有一对,你想把它平方并用平方替换以前的值,所以你说“myPair.MultiplyBy(myPair, out myPair)”。并且繁荣,你已经把自己放入了这个可怕的陷阱。一路上的每一步都很合理,但它们加起来很可怕。
  • @BillAskaga:我同意人们经常对值和变量之间的差异感到困惑,但我不明白如何将责任归咎于这个答案;这个问题似乎更加普遍和普遍。我认为你错过了我回答的重点。我并不是说 value 发生了变异,正如您正确指出的那样,值只是值;我要指出的是,即使在构造函数之外,也可能会观察到只读字段的内容随时间变化,这是违反直觉的。
  • @EricLippert:我只是认为您应该更清楚地解释 x/y 字段确实没有改变,但基础 this 参考确实这会导致 x/y 字段看似发生变化。所以主要问题是 C# 不允许我们将 myPair 字段设置为只读 - 从而阻止我们首先重新分配它。当 Pair 是一个类时,相同的代码行为不同的事实当然是非常违反直觉的。
【解决方案2】:

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

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

    // Constructor and stuff
}

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

正如Eric Lippert's answer 中所述,这不会阻止结构本身被完全重新分配,从而提供其字段从您下方更改的效果。通过值传递或使用新的in 参数修饰符可以帮助防止这种情况:

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

【讨论】:

  • 创建结构readonly 不会将所有字段隐式标记为readonly。您仍然必须自己执行此操作,但让编译器帮助您找到您忘记的任何情况。否则你会得到错误 CS8340 'Instance fields of readonly structs must be readonly.'
【解决方案3】:

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

【讨论】:

  • 当然,为了简单起见,我把它省略了。
  • 如果它们是不可变的类而不是整数呢?应该也可以吧?
  • B^) 构造函数会很有帮助。除非你一直希望 x 和 y 为零。
【解决方案4】:

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

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

【讨论】:

  • 编译器能够在编译时检测到readonly 字段的赋值,如果检测到,将引发编译器错误。
  • 很高兴知道。编辑了我的答案。
  • 运行时也将强制执行readonly 语义。也就是说,如果xMyStruct类型的readonly字段,而sMyStruct的实例,那么dynamic d = s; d.x = 42;会抛出异常。
  • 什么是“公共接口原因”?
猜你喜欢
  • 2014-09-09
  • 1970-01-01
  • 2013-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多