包含可变引用类型成员的值类型是否被视为不可变?

3

我知道可变结构体是有害的。但请考虑以下struct

public struct Foo
{
    private readonly StringBuilder _sb;

    public Foo(StringBuilder sb)
    {
        _sb = sb;
    }

    public StringBuilder Sb
    {
        get { return _sb; }
    }
}

在我看来,Foo 看起来是不可变的,因为当你运行 var foo = new Foo(new StringBuilder()) 时,你无法重新分配 Sb 属性,因为它是只读的。

然而,你可以执行以下操作:

var foo = new Foo(new StringBuilder());
foo.Sb.Append("abc"); // _sb is also modified

这会影响foo的内部状态。

问题: 这是否被认为是良好的struct设计,或者我应该避免在值类型中具有可变引用类型成员?


顺便提一下,我建议将“_sb”标记为“readonly”,以确保您不会意外破坏其不可变性。 - dcastro
@dcastro 很好的发现,现在正在添加。 - rexcfnghk
2个回答

4

这被称为浅不可变性。

请看Joe Duffy的解决多线程代码中的11个常见问题(我强调)中的不可变性部分:

例如,一个只有只读字段的.NET类型是浅不可变的。(...)更进一步,如果每个字段本身又引用另一个类型,该类型的所有字段都是只读的(并且仅引用深度不可变类型),那么该类型就是深度不可变的。这会导致整个对象图形保证不会从底层更改,这确实非常有用。

如果您使结构体不可变以防止客户端尝试修改作为参数传递的结构体的副本而不是原始值,则浅不可变性是可以的。原始值和副本都将指向同一个StringBuilder

但是,如果您这样做是为了同步目的,那么它并不会真正有帮助。

以下内容来自Eric Lippert的C#中的不可变性第一部分:不可变性的种类:

真正意义上的不可变对象具有许多优点。例如,它们是100%线程安全的,因为读者和(不存在的)写入器之间显然不会发生冲突。与可以更改的对象相比,它们更容易理解。但是,它们的严格要求可能超出我们的需求,或者超出实现的实际可行性。


struct 设计中,浅不可变性是否被接受或广泛使用? - rexcfnghk
@rexcfnghk 这是一个好问题,但我不能给你一个明确的答案。我认为它并不被广泛使用,因为通常可以避免这种情况。如果你能够避免这种情况,并使你的类型深度不可变,那就这么做吧。但如果你不能,只要记住你将无法享受一些深度不可变类型(结构体或其他)提供的好处,即线程安全性。 - dcastro
2
@rexcfnghk 我已经更新了我的答案。阅读Eric的博客,我相信他同意深度不可变性不是必须遵循的规则,因为它并不总是必需的。这是一个“好事”,而不是“必须的”。 - dcastro

1

你的设计规则应该根据你和其他人清晰地理解你的选择能力来制定。

以下是我的规则:我尽力保持我的结构体只包含简单的复合元素,如向量,点,并且仅使用不应分开更改的值类型。

如果我需要在struct中使用集合或引用类型,它公开了可能会改变其状态的成员,我会将其作为私有成员,并围绕其编写功能方法以获取需要的内容。因为我必须防止它被从外部修改。

如果我的struct中的某些内容需要修改,则认为struct不符合需求。

同样,没有什么可以阻止您通过使用浅层不可变性来欺骗struct,就像dcastro的答案学习到的那样。但是,因为struct并非设计用于此类操作,这可能会变得混乱。

我喜欢Eric Lippert的文章,它让我想起了struct应该如何工作。


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