我经常读到 struct
s 应该是不可变的 - 它们不是根据定义吗?
你考虑吗int
是不可变的?
int i = 0;
i = i + 123;
看起来不错 - 我们得到了一个新的
int
并将其分配回 i
.那这个呢?i++;
好的,我们可以把它看作是一条捷径。
i = i + 1;
struct
呢? Point
?Point p = new Point(1, 2);
p.Offset(3, 4);
这真的改变了观点
(1, 2)
?我们不应该把它看作是以下 Point.Offset()
的快捷方式吗?返回一个新点?p = p.Offset(3, 4);
这个想法的背景是这样的——一个没有身份的值类型怎么可能是可变的?您必须至少查看两次以确定它是否已更改。但是没有身份你怎么能做到这一点呢?
我不想通过考虑
ref
来使推理复杂化参数和装箱。我也知道 p = p.Offset(3, 4);
比 p.Offset(3, 4);
更好地表达不变性做。但问题仍然存在——根据定义,值类型不是不可变的吗?更新
我认为至少涉及两个概念 - 变量或字段的可变性以及变量值的可变性。
public class Foo
{
private Point point;
private readonly Point readOnlyPoint;
public Foo()
{
this.point = new Point(1, 2);
this.readOnlyPoint = new Point(1, 2);
}
public void Bar()
{
this.point = new Point(1, 2);
this.readOnlyPoint = new Point(1, 2); // Does not compile.
this.point.Offset(3, 4); // Is now (4, 6).
this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
}
}
在示例中,我们必须字段 - 一个可变的和一个不可变的。因为值类型字段包含整个值,所以存储在不可变字段中的值类型也必须是不可变的。我仍然对结果感到非常惊讶 - 我没想到 readonly 字段保持不变。
变量(除了常量)总是可变的,因此它们意味着对值类型的可变性没有限制。
答案似乎不是那么直接,所以我将重新表述这个问题。
鉴于以下。
public struct Foo
{
public void DoStuff(whatEverArgumentsYouLike)
{
// Do what ever you like to do.
}
// Put in everything you like - fields, constants, methods, properties ...
}
能不能给个完整版的
Foo
和一个使用示例 - 可能包括 ref
参数和装箱 - 这样就不可能重写所有出现的foo.DoStuff(whatEverArgumentsYouLike);
和
foo = foo.DoStuff(whatEverArgumentsYouLike);
最佳答案
An object is immutable if its state doesn’t change once the object has been created.
简短回答:不,根据定义,值类型不是不可变的。 结构体和类都可以是可变的或不可变的。 所有四种组合都是可能的。如果结构或类具有非只读公共(public)字段、带有 setter 的公共(public)属性或设置私有(private)字段的方法,则它是可变的,因为您可以在不创建该类型的新实例的情况下更改其状态。
长答案:首先,不变性问题仅适用于具有字段或属性的结构或类。最基本的类型(数字、字符串和 null)本质上是不可变的,因为它们没有任何(字段/属性)可以改变。 A 5 is a 5 is a 5. 对 5 的任何操作只会返回另一个不可变值。
您可以创建可变结构,例如
System.Drawing.Point
.两者 X
和 Y
具有修改结构字段的 setter :Point p = new Point(0, 0);
p.X = 5;
// we modify the struct through property setter X
// still the same Point instance, but its state has changed
// it's property X is now 5
有些人似乎将不变性与值类型通过值(因此它们的名称)而不是通过引用传递这一事实混淆。void Main()
{
Point p1 = new Point(0, 0);
SetX(p1, 5);
Console.WriteLine(p1.ToString());
}
void SetX(Point p2, int value)
{
p2.X = value;
}
在这种情况下 Console.WriteLine()
写“{X=0,Y=0}
”。这里p1
未被修改,因为 SetX()
修改 p2
这是 p1
的副本.这是因为 p1
是值类型,不是因为它是不可变的(不是)。为什么值类型应该是不可变的?原因很多……见this question .主要是因为可变值类型会导致各种不那么明显的错误。在上面的例子中,程序员可能已经预料到
p1
成为 (5, 0)
调用后SetX()
.或者想象一下按稍后可以更改的值进行排序。那么您的排序集合将不再按预期排序。字典和散列也是如此。 Fabulous Eric Lippert ( blog ) 写了一个 whole series about immutability以及为什么他认为这是 C# 的 future 。 Here's one of his examples这使您可以“修改”只读变量。更新:你的例子:
this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
正是 Lippert 在他的关于修改只读变量的帖子中提到的内容。 Offset(3,4)
居然修改了一个Point
,但它是 readOnlyPoint
的副本,并且它从未分配给任何东西,所以它丢失了。这就是为什么可变值类型是邪恶的:它们让您认为您正在修改某些东西,而有时您实际上是在修改副本,这会导致意外错误。如
Point
是不可变的,Offset()
将不得不返回一个新的 Point
,您将无法将其分配给 readOnlyPoint
.然后你会说“哦,对了,它是只读的,这是有原因的。我为什么要改变它?幸好编译器现在阻止了我。”更新:关于你改写的请求......我想我知道你在说什么。在某种程度上,您可以“认为”结构在内部是不可变的,修改结构与用修改后的副本替换它是一样的。据我所知,它甚至可能是 CLR 在内存中内部执行的操作。 (这就是闪存的工作原理。你不能只编辑几个字节,你需要将一整块千字节读入内存,修改你想要的几个,然后写回整个块。)但是,即使它们是“内部不可变的” ",这是一个实现细节,对于我们作为结构用户的开发人员(他们的接口(interface)或 API,如果你愿意的话),它们是可以改变的。我们不能忽视这个事实并“认为它们是不可变的”。
在评论中,您说“您不能引用字段或变量的值”。您假设每个 struct 变量都有不同的副本,这样修改一个副本不会影响其他副本。这并不完全正确。以下标记的行不可替换,如果...
interface IFoo { DoStuff(); }
struct Foo : IFoo { /* ... */ }
IFoo otherFoo = new Foo();
IFoo foo = otherFoo;
foo.DoStuff(whatEverArgumentsYouLike); // line #1
foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2
第 1 行和第 2 行没有相同的结果......为什么?因为 foo
和 otherFoo
引用相同的 Foo 盒装实例。无论在 foo
中发生了什么变化第 1 行反射(reflect)在 otherFoo
.第 2 行替换 foo
使用新值并且对 otherFoo
没有任何作用(假设 DoStuff()
返回一个新的 IFoo
实例并且不修改 foo
本身)。Foo foo1 = new Foo(); // creates first instance
Foo foo2 = foo1; // create a copy (2nd instance)
IFoo foo3 = foo2; // no copy here! foo2 and foo3 refer to same instance
修改 foo1
不会影响foo2
或 foo3
.修改 foo2
将反射(reflect)在 foo3
,但不在 foo1
.修改 foo3
将反射(reflect)在 foo2
但不在 foo1
.令人困惑?坚持不可变的值类型,你就消除了修改它们中的任何一个的冲动。
更新:修复了第一个代码示例中的错字
关于c# - 根据定义,值类型是不可变的吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/868411/