不可变值类型

时间:2011-07-04 08:54:37

标签: c# .net clr

我正在阅读Eric Liperts关于Mutating Readonly Structs的博客,我在这篇博客中看到很多参考资料作为一个论证,为什么价值类型必须是不可变的。 但仍然有一点不清楚,说当你访问值类型时,你总是得到它的副本,这是一个例子:

struct Mutable
{
    private int x;
    public int Mutate()
    {
        this.x = this.x + 1;
        return this.x;
    }
}

class Test
{
    public readonly Mutable m = new Mutable();
    static void Main(string[] args)
    {
        Test t = new Test();
        System.Console.WriteLine(t.m.Mutate());
        System.Console.WriteLine(t.m.Mutate());
        System.Console.WriteLine(t.m.Mutate());
    }
}

这就是为什么我改变

的原因
public readonly Mutable m = new Mutable();

public Mutable m = new Mutable();

一切都开始起作用预期

请您更清楚地解释为什么值类型必须是不可变的。 我知道它对线程安全有好处,但在这种情况下,同样可以应用于引用类型。

4 个答案:

答案 0 :(得分:4)

使用变异方法的结构在几种情况下表现得很奇怪。

您已经发现的示例是一个只读字段。防御性副本是必要的,因为您不想改变只读字段。

但也用作属性。再次发生隐式复制,只复制副本。即使该物业有一个二传手。

struct Mutable
{
    private int x;
    public int Mutate()
    {
        this.x = this.x + 1;
        return this.x;
    }
}

Mutable property{get;set;}

void Main()
{
    property=new Mutable();
    property.Mutate().Dump();//returns 1
    property.Mutate().Dump();//returns 1 :(
}

这表明变异方法在结构上是有问题的。但它没有表明具有公共字段的可变结构或具有setter的属性存在问题。

答案 1 :(得分:2)

线程安全是一个明确的技术原因。它适用于值类型以及引用类型(请参阅System.String)。

更一般的指导方针“价值类型应该是不可变的”是不同的。它是关于代码的可读性,主要来自可变值可能导致的混淆。此代码段只是一个示例。大多数人不会期望1,1,1结果。

答案 2 :(得分:2)

我不知道C#所以我会尝试回答你问题的第二部分。

为什么值类型必须是不可变的?

Domain Driven Design的观点有两种类型的对象:

  • 值对象/类型 - 他们的身份由他们的值决定(例如数字:2总是2 - 数字2的身份总是相同,所以2 == 2总是如此)
  • 实体(引用类型) - 它们可以包含其他值类型,并且它们的身份由其身份本身决定(例如人:即使有人看起来与您完全一样,也不会是你)

如果值类型是可变的,那么想象如果有可能改变第二个值的话会发生什么:2 == 1 + 1不能保证是真的。

请参阅以下链接了解更多信息:

答案 3 :(得分:0)

我认为关于这个例子的棘手问题是人们可能认为它不应该是可能的。你创建了一个只读的Mutable实例,但你可以通过Mutate()函数改变它的值,因此在某种意义上违反了不变性的概念。但是,严格来说,它的工作原理是因为私有字段x不是只读的。如果你在可变类中做了一个简单的改变,那么实际上就会强制实现不变性:

private readonly int x;

然后Mutate()函数将产生编译器错误。

该示例清楚地显示了copy-by-value如何在只读变量的上下文中工作。无论何时调用m,您都要创建实例的副本,而不是实例引用的副本 - 如果Mutable是类而不是结构,则会发生后者。

因为每次调用m时你都会调用1)实例的副本,以及2)只读实例的副本,x的值在总是为0时复制发生。当你在副本上调用Mutate()时,它会将x递增到1,这是因为x本身不是只读的。但是下次调用Mutate()时,你仍然会调用默认值为0.正如他在文章中所说的那样“m是不可变的,但副本不是”。原始实例的每个副本都将x设置为0,因为正在复制的对象永远不会更改,而其副本可以更改。

也许这有帮助。