在C#中,值类型是可变的还是不可变的?

19

值类型的行为表明我们所持有的任何值都不能通过其他变量进行更改。

但是,我仍然对我在帖子标题中提到的内容感到困惑。有人可以澄清吗?

4个回答

27

值类型可以是可变的或者(除了一些奇怪的边界情况)是不可变的,这取决于你如何编写它们。

可变的:

public struct MutableValueType
{
    public int MyInt { get; set; }
}

不可变:

public struct ImmutableValueType
{
    private readonly int myInt;
    public ImmutableValueType(int i) { this.myInt = i; }

    public int MyInt { get { return this.myInt; } }
}

内置值类型(如intdouble等)是不可变的,但你可以很容易地创建自己的可变struct

一个建议: 不要。可变值类型是一个坏主意,应该避免使用。例如,以下代码会做什么:

SomeType t = new SomeType();
t.X = 5;

SomeType u = t;
t.X = 10;

Console.WriteLine(u.X);

这取决于具体情况。如果SomeType是一个值类型,它将打印5,这是一个相当令人困惑的结果。

关于为什么应该避免可变值类型,请参考此问题了解更多信息。


1
@dlev,当你有以下内容时。int i = 5; i = 5+2; 5+2被传递到一个新的int中,并将引用返回给i?在我看来,当SomeType打印5时,我不会感到困惑,但当SomeType打印10时,我就感到困惑了。好答案。 - Jethro
1
@dlev,但是通过改变<code>i</code>所持有的内存,我们不是改变了<code>i</code>的值,使其可变吗?我一直知道<code>Strings</code>是不可变的,因此当你做这样的事情时<code>str = str + "change";</code>实际上是调用了<code>String.Concat</code>,它创建了一个新的内存块,然后将str指向该内存块。这是否也适用于<code>i</code>,或者整数的原始内存块会被更改?希望这样说得清楚。 - Jethro
2
在你的例子中,如果SomeType是一个具有整数字段X的值类型,u.X将为5。这些信息和上面的代码足以显示该信息;它应该很难被“意外”。现在假设t是一个类类型,并且在创建t和WriteLine调用之间的任何地方 - 即使在“t.x = 10”之前 - 有一个对SomeInheritableClassObject.SomeVirtualFunction(t)的调用。除非检查* SomeVirtualFunction的所有可能覆盖*,否则无法知道WriteLine会输出什么。我认为可变结构的可预测行为要好得多。 - supercat
我对你的“不可变”的例子表示怀疑,因为MSDN上说:当属性 getter 返回值类型时,调用者会收到一个副本。因为副本是隐式创建的,开发人员可能没有意识到他们正在改变副本,而不是原始值。此外,一些语言(特别是动态语言)使用可变值类型会遇到问题,因为即使是局部变量在解引用时也会导致复制的产生。 https://msdn.microsoft.com/en-us/library/ms229031%28v=vs.110%29.aspx - ElektroStudios
我不认为这是一个有说服力的例子 - 为什么要写出这样的代码呢?我的意思是:你将其作为可变值类型不好的例子,但引用类型变量的情况也同样糟糕 - 当你读取u.X并尝试弄清楚那可能是什么时,你可能会看到uX=5 初始化且从未修改过,并且可能会错误地得出u.X也是5的结论。通过混合别名和突变,现在您需要玩(不可能的)跟随所有这些别名的游戏......如果“SomeType”在内部使用类似的技巧,祝你好运... - Eamon Nerbonne
显示剩余6条评论

3

所有的原始值类型,如int、double和float都是不可变的。但结构体本身是可变的。因此,您必须采取措施使它们尽可能不可变,以避免造成许多混淆。


好的,但是引用类型呢?在阅读文档时我了解到它们可以是可变或不可变的。这是怎么回事? - Kuntady Nithesh
@Nitesh。这也取决于您如何实现它。System.Text.StringBuilder类是可变引用类型的示例。它包含可以更改类的实例值的成员。不可变引用类型的示例是System.String类。在实例化后,其值永远不会更改。 - Ashley John

1
任何保存信息的值类型实例都可以被代码改变,只要该代码能够写入其所在的存储位置。而任何不能写入其所在存储位置的代码都无法改变值类型实例。这些特性使得可变值类型的私有存储位置在许多情况下成为理想的数据容器,因为它们结合了源自可变性的更新便利性和源自不可变性的控制。请注意,可以编写值类型的代码,使得在没有包含所需数据的实例(例如新创建的临时实例)之前,无法更改现有实例,然后用后者的内容覆盖前者的内容,但这并不会使值类型比没有此类能力时更加可变或不可变。在许多情况下,这只是使突变变得笨拙,并使其看起来像是一个语句:

将创建一个新实例,但不会影响现有实例。如果KeyValuePair是一个不可变类,并且一个线程正在执行MyKeyValuePair.ToString()而另一个线程正在执行上述代码,则ToString调用将作用于旧实例或新实例,并因此产生既有旧值也有新值的结果。然而,由于KeyValuePair是一个结构体,上述语句将创建一个新实例,但它不会使MyKeyValuePair引用新实例——它只会使用新实例作为模板,其字段将被复制到MyKeyValuePair中。如果KeyValuePair是一个可变结构体,则上述代码可能意图表达的最自然表达方式更像是:

  MyKeyValuePair.Key += 1;
  MyKeyValuePair.Value += 1;

或者:

  var temp = MyKeyValuePair;
  MyKeyValuePair.Key = temp.Key+1;
  MyKeyValuePair.Value = temp.Value+1;

这样,线程的影响将更加清晰。


0
我想知道这里的一些回答者是否忽略了一个值得一提的观点。
我可能有一个错误的印象,如果我错了,请纠正我;但我理解的一个关键区别是,值类型和引用类型之间的一个关键区别是,值类型可以用来建立一个可引用的同质连续内存块。(请记住,在“fixed”和“span”以及所有为方便和强大使用“不安全”代码段而做的努力之前,结构体早已存在。)
一个类的数组,即使是具有值类型成员的小类,也是一个指针数组。在迭代并引用每个类的成员时,CPU可能需要每次都到主内存。
另一方面,结构体的数组是在数组内实现的。它是一个连续的大内存块。在迭代过程中,可以更好地利用CPU缓存。非常多。
如果这是您使用结构体容器的原因(我认为这可能是我使用结构体的主要原因),那么您可能希望能够在该内存块中引用和更新单个结构体成员,而无需CPU一次创建一个或多个整个结构体的副本。
在这整个情况类别中,我认为这实际上是C#结构存在的动机,使结构成员只读或不可变很可能完全违背了初衷。
我想有些人可能会对我嘲笑,说如果我这么担心性能,就不应该使用C#;但正如Mike Acton所说的那样,“人们不关心性能就是为什么我的Word需要30秒才能启动。”

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