Dan*_*ner 40 .net c# immutability value-type
我经常读到structs应该是不可变的 - 根据定义它们不是吗?
你认为int是不可改变的吗?
int i = 0;
i = i + 123;
Run Code Online (Sandbox Code Playgroud)
似乎没关系 - 我们得到一个新的int并将其分配给i.那这个呢?
i++;
Run Code Online (Sandbox Code Playgroud)
好的,我们可以把它想象成一条捷径.
i = i + 1;
Run Code Online (Sandbox Code Playgroud)
怎么样struct Point?
Point p = new Point(1, 2);
p.Offset(3, 4);
Run Code Online (Sandbox Code Playgroud)
这真的改变了这一点(1, 2)吗?我们难道不应该将它视为Point.Offset()返回新点的下列捷径吗?
p = p.Offset(3, 4);
Run Code Online (Sandbox Code Playgroud)
这种想法的背景是这样的 - 没有身份的价值类型怎么可能是可变的?您必须至少查看两次以确定它是否发生了变化.但是如果没有身份,你怎么能这样做呢?
我不想通过考虑ref参数和拳击来使这个推理复杂化.我也知道,p = p.Offset(3, 4);表达不变性比做得好p.Offset(3, 4);.但问题仍然存在 - 根据定义,值不是不可变的值吗?
UPDATE
我认为至少涉及两个概念 - 变量或字段的可变性以及变量值的可变性.
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).
}
}
Run Code Online (Sandbox Code Playgroud)
在示例中,我们必须使用字段 - 可变字段和不可变字段.因为值类型字段包含整个值,所以存储在不可变字段中的值类型也必须是不可变的.我对结果仍然感到非常惊讶 - 我没有想到readonly字段保持不变.
变量(除了常量)总是可变的,因此它们意味着对值类型的可变性没有限制.
答案似乎不是那么直接,所以我会重新解释这个问题.
鉴于以下内容.
public struct Foo
{
public void DoStuff(whatEverArgumentsYouLike)
{
// Do what ever you like to do.
}
// Put in everything you like - fields, constants, methods, properties ...
}
Run Code Online (Sandbox Code Playgroud)
你能给出一个完整版本Foo和一个用法示例 - 可能包括ref参数和装箱 - 这样就不可能重写所有出现的
foo.DoStuff(whatEverArgumentsYouLike);
Run Code Online (Sandbox Code Playgroud)
同
foo = foo.DoStuff(whatEverArgumentsYouLike);
Run Code Online (Sandbox Code Playgroud)
Luc*_*cas 45
如果对象在创建对象后状态不会更改,则该对象是不可变的.
简答:不,根据定义,值类型不是不可变的.结构和类都可以是可变的或不可变的.所有四种组合都是可能的.如果结构或类具有非只读公共字段,具有setter的公共属性或设置私有字段的方法,则它是可变的,因为您可以在不创建该类型的新实例的情况下更改其状态.
答案很长:首先,不变性问题仅适用于具有字段或属性的结构或类.最基本的类型(数字,字符串和null)本质上是不可变的,因为没有任何东西(字段/属性)可以改变它们.A 5是5是5.对5的任何操作只返回另一个不可变值.
你可以创建可变的结构,如System.Drawing.Point.双方X并Y具有修改结构的领域制定者:
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
Run Code Online (Sandbox Code Playgroud)
有些人似乎把不可靠性与价值类型通过价值(因此他们的名字)而不是通过引用传递的事实相混淆.
void Main()
{
Point p1 = new Point(0, 0);
SetX(p1, 5);
Console.WriteLine(p1.ToString());
}
void SetX(Point p2, int value)
{
p2.X = value;
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下Console.WriteLine()写" {X=0,Y=0}".这里p1没有修改,因为SetX()修改p2这是一个复制的p1.这p1是因为它是一个值类型,而不是因为它是不可变的(它不是).
为什么要值类型是不可变的?很多原因......看到这个问题.主要是因为可变值类型会导致各种不那么明显的错误.在上面的例子中,程序员可能期望p1将(5, 0)调用后SetX().或者想象一下可以在以后改变的值进行排序.然后,您的已排序集合将不再按预期排序.字典和哈希也是如此.在美妙的埃里克利珀(博客)写了一本关于不变性全系列,以及为什么他认为这是C#的未来.这是他的一个例子,它允许你"修改"一个只读变量.
更新:您的示例:
this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
Run Code Online (Sandbox Code Playgroud)
正是Lippert在帖子中提到的关于修改只读变量的内容.Offset(3,4)其实修改的Point,但它是一个复制的readOnlyPoint,它从来没有分配到任何东西,所以它的丢失.
而这也就是为什么可变的值类型是邪恶的:他们让你觉得你正在修改的东西,有时,当你实际上是修改副本,从而导致意外的错误.如果Point是不可变的,Offset()则必须返回一个新的Point,并且您无法将其分配给readOnlyPoint.然后你去"哦,对,它是只读的有一个原因.为什么我要改变它?好的事情,编译器现在阻止了我."
更新:关于你的改写请求......我想我知道你得到了什么.在某种程度上,您可以"认为"结构是内部不可变的,修改结构与将其替换为修改后的副本相同.就我所知,它甚至可能就是CLR在内存中所做的事情.(这就是闪存的工作原理.你不能只编辑几个字节,你需要将整块KB读入内存,修改你想要的几个,然后再写回整个块.)但是,即使它们是"内部不可变的" ",这是一个实现细节,对于我们的开发人员来说,作为结构的用户(他们的界面或API,如果你愿意的话),他们可以被改变.我们不能忽视这一事实并"将它们视为不可改变的".
在评论中,您说"您不能引用字段或变量的值".您假设每个结构变量都有不同的副本,因此修改一个副本不会影响其他副本.这并非完全正确.如果......,下面标出的线不可更换
interface IFoo { DoStuff(); }
struct Foo : IFoo { /* ... */ }
IFoo otherFoo = new Foo();
IFoo foo = otherFoo;
foo.DoStuff(whatEverArgumentsYouLike); // line #1
foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2
Run Code Online (Sandbox Code Playgroud)
第1行和第2行的结果不一样......为什么?因为foo并且otherFoo引用了同一个盒装的Foo 实例.foo第1行中的任何变化都反映在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
Run Code Online (Sandbox Code Playgroud)
修改foo1不会影响foo2或foo3.修改foo2将反映在foo3,但不在foo1.修改foo3将反映在foo2但不在反映中foo1.
混乱?坚持不可变的值类型,你消除了修改其中任何一个的冲动.
更新:修复了第一个代码示例中的拼写错误
您可以编写可变的结构,但最佳做法是使值类型不可变.
例如,DateTime总是在执行任何操作时创建新实例.点是可变的,可以改变.
回答你的问题:不,它们不是定义不可变的,它取决于它们是否应该是可变的情况.例如,如果它们应该作为字典键,它们应该是不可变的.
如果你的逻辑足够远,那么所有类型都是不可变的.当您修改引用类型时,您可能会认为您实际上是在将新对象写入同一地址,而不是修改任何内容.
或者你可以说任何语言中的一切都是可变的,因为偶尔用于一件事的记忆会被另一个人覆盖.
有了足够的抽象,忽略了足够的语言功能,你可以得到任何你喜欢的结论.
这就错过了重点.根据.NET规范,值类型是可变的.你可以修改它.
int i = 0;
Console.WriteLine(i); // will print 0, so here, i is 0
++i;
Console.WriteLine(i); // will print 1, so here, i is 1
Run Code Online (Sandbox Code Playgroud)
但它仍然是一样的我.该变量i仅声明一次.在此声明之后发生的任何事情都是修改.
在类似具有不可变变量的函数式语言中,这是不合法的.++我不可能.声明变量后,它具有固定值.
在.NET中,情况并非如此,没有什么可以阻止我i在声明之后修改它.
在考虑了一下之后,这是另一个可能更好的例子:
struct S {
public S(int i) { this.i = i == 43 ? 0 : i; }
private int i;
public void set(int i) {
Console.WriteLine("Hello World");
this.i = i;
}
}
void Foo {
var s = new S(42); // Create an instance of S, internally storing the value 42
s.set(43); // What happens here?
}
Run Code Online (Sandbox Code Playgroud)
在最后一行,根据你的逻辑,我们可以说我们实际上构造了一个新对象,并用该值覆盖旧对象.但那是不可能的!要构造一个新对象,编译器必须将i变量设置为42.但它是私有的!它只能通过用户定义的构造函数访问,该构造函数明确禁止值43(将其设置为0),然后通过我们的set方法,它具有令人讨厌的副作用.编译器无法仅使用它喜欢的值创建新对象.s.i可以设置为43 的唯一方法是通过调用修改当前对象set().编译器不能只这样做,因为它会改变程序的行为(它会打印到控制台)
因此,对于所有结构都是不可变的,编译器必须作弊并破坏语言规则.当然,如果我们愿意违反规则,我们可以证明任何事情.我可以证明所有整数也是相同的,或者定义一个新类将导致你的计算机着火.只要我们遵守语言规则,结构就是可变的.