我经常读到struct s应该是不可变的 - 根据定义它们不是吗?
你认为int是不可变的吗?
1 2
| int i = 0;
i = i + 123; |
好像没问题 - 我们得到一个新的int并将其分配回i。那这个呢?
好的,我们可以把它想象成一条捷径。
struct Point怎么样?
1 2
| Point p = new Point (1, 2);
p .Offset(3, 4); |
这真的会改变点(1, 2)吗?我们难道不应该将它视为Point.Offset()返回新点的下列快捷方式吗?
这种想法的背景是这样的 - 没有身份的价值类型怎么可能是可变的?您必须至少查看两次以确定它是否发生了变化。但是如果没有身份,你怎么能这样做呢?
我不想通过考虑ref参数和装箱来推理这个问题。我也知道p = p.Offset(3, 4);比p.Offset(3, 4);更好地表达了不变性。但问题仍然存在 - 根据定义,值不是不可变的值吗?
UPDATE
我认为至少涉及两个概念 - 变量或字段的可变性以及变量值的可变性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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字段保持不变。
变量(除了常量)总是可变的,因此它们意味着对值类型的可变性没有限制。
答案似乎不是那么直接,所以我会重新解释这个问题。
鉴于以下内容。
1 2 3 4 5 6 7 8 9
| 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参数和装箱 - 这样就不可能重写所有出现的
1
| foo.DoStuff(whatEverArgumentsYouLike); |
同
1
| foo = foo.DoStuff(whatEverArgumentsYouLike); |
An object is immutable if its state
doesn’t change once the object has
been created.
Ok.
简答:不,根据定义,值类型不是不可变的。结构和类都可以是可变的或不可变的。所有四种组合都是可能的。如果结构或类具有非只读公共字段,具有setter的公共属性或设置私有字段的方法,则它是可变的,因为您可以在不创建该类型的新实例的情况下更改其状态。
答案很长:首先,不变性问题仅适用于具有字段或属性的结构或类。最基本的类型(数字,字符串和null)本质上是不可变的,因为没有任何东西(字段/属性)可以改变它们。 A 5是5是5.对5的任何操作只返回另一个不可变值。
您可以创建可变结构,例如System.Drawing.Point。 X和Y都有修饰结构字段的setter:
1 2 3 4 5
| 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 |
有些人似乎把不可靠性与价值类型通过价值(因此他们的名字)而不是通过引用传递的事实相混淆。
1 2 3 4 5 6 7 8 9 10 11
| 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是值类型,而不是因为它是不可变的(它不是)。
为什么值类型是不可变的?很多原因......看到这个问题。主要是因为可变值类型会导致各种不那么明显的错误。在上面的例子中,程序员在调用SetX()之后可能期望p1为(5, 0)。或者想象一下可以在以后改变的值进行排序。然后,您的已排序集合将不再按预期排序。字典和哈希也是如此。 Fabulous Eric Lippert(博客)撰写了一系列关于不变性的文章以及为什么他认为这是C#的未来。这是他的一个例子,它允许你"修改"一个只读变量。
更新:您的示例:
1
| this.readOnlyPoint.Offset(3, 4); // Is still (1, 2). |
正是Lippert在帖子中提到的关于修改只读变量的内容。 Offset(3,4)实际上修改了Point,但它是readOnlyPoint的副本,并且它从未被分配给任何东西,所以它丢失了。
这就是为什么可变值的类型是邪恶的:它们让你认为你正在修改某些东西,有时你实际上在修改副本,这会导致意外的错误。如果Point是不可变的,Offset()将不得不返回一个新的Point,并且您将无法将其分配给readOnlyPoint。然后你去"哦,对,它是只读的有一个原因。为什么我要改变它?好的事情,编译器现在阻止了我。"
更新:关于你的改写请求......我想我知道你得到了什么。在某种程度上,您可以"认为"结构是内部不可变的,修改结构与将其替换为修改后的副本相同。就我所知,它甚至可能就是CLR在内存中所做的事情。 (这就是闪存的工作原理。你不能只编辑几个字节,你需要将整块KB读入内存,修改你想要的几个,然后再写回整个块。)但是,即使它们是"内部不可变的"",这是一个实现细节,对于我们的开发人员来说,作为结构的用户(他们的界面或API,如果你愿意的话),他们可以被改变。我们不能忽视这一事实并"将它们视为不可改变的"。
在评论中,您说"您不能引用字段或变量的值"。您假设每个结构变量都有不同的副本,因此修改一个副本不会影响其他副本。这并非完全正确。如果......,下面标出的线不可更换
1 2 3 4 5 6 7
| 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实例。第1行中foo的变化反映在otherFoo中。第2行用新值替换foo并且不对otherFoo做任何操作(假设DoStuff()返回一个新的IFoo实例并且不修改foo本身)。
1 2 3
| 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将反映在foo3中,但不会反映在foo1中。修改foo3将反映在foo2中但不包含在foo1中。
混乱?坚持不可变的值类型,你消除了修改其中任何一个的冲动。
更新:修复了第一个代码示例中的拼写错误
好。
-
我以前在几个答案中添加了这个评论。我可以重写p.X = 5;如p = p.SetX(5);如果我总能这样做 - 值类型语义可能允许这样,但我不确定 - 我可以认为结构是不可变的或等同于不可变的结构。所以我改写了这个问题 - 我能不能总是进行这种转变?如果是,这意味着结构是不可变的,因为我可以以一种使不变性显而易见的方式重写它们。
-
@丹尼尔:我不确定我是否跟着你。如果你可以做p.X = 5,那么类型是可变的。如果p2 = p1.SetX(5)没有改变p1,并且无法改变p1,那么它是不可变的。请注意,p = p.SetX(5)正在用新值替换p的值,而不是修改原始值。
-
你是绝对正确的。并且因为p是值类型而您无法引用它,所以如果修改存储在p中的值或将其替换为修改后的版本,则无关紧要。如果你找到一个重要的例子 - 可能涉及参数,拳击,或者我甚至没想过的东西(目前正在考虑值类型属性) - 比我错了,结构是可变的。如果我总是可以将myStruct.ChangeStuff()转换为myStruct = myStruct.ChangeStuff(),那么我可以认为结构是不可变的。
-
我想我终于明白了你的意思!一切都在"p是一种值类型,你不能引用它",但你可以通过拳击和界面。我已经更新了我的答案。
-
内部不变性:这不是一个实现细节 - 如果你有一个带有myStruct.ChangeState()的"可变"结构,你可以重新设计myStruct = myStruct.GetCloneWithChangedState()的接口,使结构"不可变"。两个版本使用不同的接口具有完全相同的行为 - 那么为什么我应该调用一个版本mutabe和一个不可变的?拳击:我考虑装箱一个结构然后传递对象,但所有方法将对不同的未装箱值进行操作,因此调用不会修改共享值。
-
界面拳击:这是真正的答案。大!值类型不是不可变的(我应该说"行为类似于不可变类型")因为你可以通过接口装箱来共享它们的引用。谢谢!
-
@DanielBrückner:即使所谓的"不可变"值类型也可以显示为不可变,如果存储它们的存储位置在被执行时被覆盖。例如,如果在某个字段中存储了KeyValuePair的实例,并且Foo的ToString方法用新实例替换了该字段的内容,则返回的字符串将包含旧的ToString值Foo与新Bar的连接。
-
巧妙地使用接口,它是我见过的少数接口之一,证明接口非常有用,(从平均编码器的角度来看)。
可变性和值类型是两个不同的东西。
将类型定义为值类型,表示运行时将复制值而不是对运行时的引用。另一方面,可变性取决于实现,每个类可以根据需要实现它。
-
我知道这一点,但问题是如果值类型意味着不可变。
-
它并不意味着它,因为它取决于用户实现不可变性。您可以拥有一个不可变的Point类,或者实现是可变的。
-
我仍然认为你做不到。你能给出一个例子,用myStruct.DoStuuf()修改结构不能解释为myStruct = myStruct.DoStuff()吗?
-
发生这种情况是因为您正在重用变量myStruct。如果DoStuff修改了同一个实例,那么赋值不会做任何事情(它复制相同)。如果DoStuff产生另一个修改过的实例,则分配它,并用它覆盖旧的myStruct内存空间。
-
这就是我的意思 - 您可以解释修改结构,并使用从方法返回的另一个结构覆盖结构。因此,您可以认为结构是不可变的。对于参考类型,上述情况显然不正确。
-
如果myStruct是对引用类型(系统中唯一引用)的引用,则会发生同样的情况......您将拥有一个较新的对象,由同一个变量引用。
您可以编写可变的结构,但最佳做法是使值类型不可变。
例如,DateTime总是在执行任何操作时创建新实例。点是可变的,可以改变。
回答你的问题:不,它们不是定义不可变的,它取决于它们是否应该是可变的情况。例如,如果它们应该作为字典键,它们应该是不可变的。
-
您可以创建一个简单的结构,如struct Foo {public int Bar; }。问题不在于你是否可以做到这一点,但如果Foo是可变的。
-
例如,System.Drawing.Point结构不是不可变的。
-
好的,不知道框架中有任何可变结构。纠正我的回答,谢谢。
-
我可以重写point.X = 42; as point = point.SetX(42);如果我总能这样做,我可能会认为Point结构是不可变的,即使接口没有很好地显示它。
如果你的逻辑足够远,那么所有类型都是不可变的。当您修改引用类型时,您可能会认为您实际上是在将新对象写入同一地址,而不是修改任何内容。
或者你可以说任何语言中的一切都是可变的,因为偶尔用于一件事的记忆会被另一个人覆盖。
有了足够的抽象,忽略了足够的语言功能,你可以得到任何你喜欢的结论。
这就错过了重点。根据.NET规范,值类型是可变的。你可以修改它。
1 2 3 4
| 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 |
但它仍然是一样的我。变量i仅声明一次。在此声明之后发生的任何事情都是修改。
在类似具有不可变变量的函数式语言中,这是不合法的。 ++我不可能。声明变量后,它具有固定值。
在.NET中,情况并非如此,没有什么可以阻止我在声明之后修改i。
在考虑了一下之后,这是另一个可能更好的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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?
} |
在最后一行,根据你的逻辑,我们可以说我们实际上构造了一个新对象,并用该值覆盖旧对象。
但那是不可能的!要构造一个新对象,编译器必须将i变量设置为42.但它是私有的!它只能通过用户定义的构造函数访问,该构造函数明确禁止值43(将其设置为0),然后通过我们的set方法,它具有令人讨厌的副作用。编译器无法仅使用它喜欢的值创建新对象。 s.i可以设置为43的唯一方法是通过调用set()修改当前对象。编译器不能只这样做,因为它会改变程序的行为(它会打印到控制台)
因此,对于所有结构都是不可变的,编译器必须作弊并破坏语言规则。当然,如果我们愿意违反规则,我们可以证明任何事情。我可以证明所有整数也是相同的,或者定义一个新类将导致你的计算机着火。
只要我们遵守语言规则,结构就是可变的。
-
也许是迄今为止最好的答案。但我认为这是两个概念 - 变量的可变性和变量值的可变性。要考虑一下...... +1
-
添加另一个例子
-
我已经重写了它...我修改了你的例子 - 如果我介绍了你不想要的东西,请撤消这个,但我相信你真的想把字段设置为43。
-
实际上,"设置"功能是错误的。它显然应该只设置私有"i"字段而不是其他。没有返回值。
-
我再次删除了你的例子并修改了我的帖子以使我的观点更清晰。也许你因为上述错误而误解了我的例子?我想要展示的是,如果结构是不可变的,编译器必须实例化一个新的S对象来覆盖我在Foo()的第一行显式创建的对象。但是编译器无法创建S的实例,其中"i"字段= 43,因此它不能随意创建新实例。它必须修改现有的,因为这是"我"可以设置为43的唯一方式。
-
我忘记在编辑中添加一些东西 - 我不是在谈论让编译器重写代码。我只是在谈论结构及其界面的手动重新设计,因此你的例子和我的重写版本之间存在很大的差异。
-
但这证明了什么?是的,我们总是可以重写我们的代码以使用不可变的结构,但这并不意味着每个结构都是不可变的。 :)
-
不,可变引用类型和值类型之间存在巨大差异。如果我有两个类型StringBuilder的变量并且我将一个变量复制到另一个变量,那么我对第一个目标(例如var1.Append("Fred"))所做的任何更改都会影响第二个变量。这远远不等于创建一个新实例并使第一个变量指向它(例如var1 = new StringBuilder(var1.ToString +"Fred"))。
-
我从未说过值类型与可变引用相同。我只是说价值类型是可变的。 struct S { public int i = 42; } S s; s.i = 43;我修改了S结构的成员。如果值类型是不可变的,这是不可能的。
-
@jalf:值类型具有与可变引用类型或不可变引用类型不同的语义。如果我有一个StringBuilder类型的私有字段,它包含对StringBuilder实例的引用,而另一个引用同一StringBuilder实例的其他字符会更改第一个字符,那么StringBuilder实例的第一个字符没有任何东西写入我的领域将会改变。相比之下,如果我有一个Point类型的字段,它的X属性将改变的唯一方法是是否有东西写入该字段。
-
@jalf:在很多方面,结构行为更像是不可变类而不是可变类(字符串是不可变的,但是可以编写string字段的代码foo可以使foo的第一个字符成为"X"存储对以"X"开头的字符串的引用。最大的区别在于struct方法通过引用而不是通过值接收底层存储位置,更新struct字段比创建新实例的成本更低,并且结构的线程语义与类的不同。
-
@supercat:如果你可以在生命周期内更新其内存位置的数据,那么对象究竟是多么不可变?这是一个荒谬的论点。值类型的行为就像它们是可变的:我可以为它们分配新值,我可以修改它们的字段。这意味着它们是可变的。如果我有一个包含字段的对象,并且我可以修改该字段的值,那么包含该字段的对象是可变的。
-
字符串是不可变的,因为给定一个字符串,我永远不会改变它的任何字段。对字符串的引用是可变的,因为我可以更改它指向的字符串。包含公共字段的结构是可变的,因为我可以在不创建新结构的情况下修改该公共字段,更改结构的值
-
@jalf:结构体的行为在很多方面比不可变的类对象更接近于可变的类对象。如果存储位置具有可变性,则无论存储位置是结构还是类,该存储位置的正常实现属性都将能够更改;可变的或不可变的。如果存储位置是不可变的,那么只有存储位置包含可变类(或者属性访问其他地方定义的可变类)时,其属性才能更改。
-
@jalf:使用不可变类的主要好处是,只能通过可写入该存储位置的代码来更改不可变类类型的存储位置的属性。同样的保证同样适用于结构(无论它们是否允许分段变异),也就是说结构是否可变,它们共享不可变类的最有用的特征。
-
@supercat:什么?你在胡说八道。你是说不可变类的主要好处是它们是可变的吗?那些(某些)代码可以改变他们的存储?不计算!不可变类是不能变异的类。故事结局。字符串是不可变的,因为给定字符串实例,您无法修改它。您可以将其换成其他实例,但您永远不能修改它。这就是不可改变的意思。您可以决定引用不同的实例(通过将其分配给字符串引用),但字符串本身是不可变的
-
对于值不可变的值类型,同样必须为真。给定一个结构的实例,它一定不可能改变它。你可以争辩说,如果我做了类似int i = 0; i = 1的事情,那么我就不会改变int,我正在创建一个新的。这不是CLR实际上做的事情,但没关系。无论我们是否认为int要变异,效果都是一样的。但是,如果定义具有一个或多个可修改字段的结构,则在更新其中一个字段时,外部结构不会替换为其他实例。因此它是可变的
-
你曾经使用过变量不可变的语言吗?因为你真的觉得你实际上并不知道这个词是什么意思
-
@jalf:如果对它们的引用无法存储在可变存储位置,那么不可变类将毫无用处。像string这样的不可变类的好处是,类型string的存储位置(例如,字段foo)的语义内容只能在存储位置本身的情况下改变。如果foo最初持有对字符串"Fred"的引用,那么foo.Length将等于4.如果我执行语句foo ="George",那么foo.Length将等于5.可以更改foo的代码 - 并且仅可以更改foo的代码 - 可以更改foo.Length。
-
@jalf:相比之下,如果我有一个StringBuilder类型的字段Bar,那么除非它拥有我自己创建的实例的引用并且没有与外部代码共享,否则我无法知道可能会改变。外部代码将能够更改Bar.Length,无论它是否具有对Bar的任何访问权限,而无需向我的代码发出任何通知或警告。这是结构的可变性级别,幸运的是没有结构(除非它们包含可变类类型的字段)。
-
你注意到了除了这个词吗?你是说根据定义,结构是不可变的,即使你自己知道一个与它相矛盾的异常吗?我想我们已经在这里完成了。对不起,我不能说这个逻辑。根据定义,为什么我们不说价值类型是三明治,即使它们实际上是紫色的?
aren't value types immutable by definition?
不,他们不是:例如,如果你看一下System.Drawing.Point结构,它的X属性上有一个setter和一个getter。
但是,可以说所有值类型都应该使用不可变API来定义。
-
是的,它有setter,但我可以重写point.X = 42; as point = point.SetX(42) - 问题是这是否总是可行的。如果是,您可以认为结构是不可变的(但是接口不能很好地表达这种不变性)。
-
如果类型的目的是封装固定的自变量集合(例如点的坐标),则最佳实现是具有公开的公共字段的结构(其将表现为固定的自变量集合)。人们可以使用不可变类来笨拙地实现这种行为,并且可以以这样的方式编写结构,以便像使用不可变类一样笨拙,但如果一个人的目标是用管道类型封装一组固定的变量,为什么不使用实现的数据类型,并且行为完全像所期望的那样?
-
@supercat主要的问题是带有set属性的结构,它允许你执行类似point.X += 3的操作,这样做不符合你的期望;而要求你说point.SetX(point.X + 3)的API并不容易出错。
-
@ChrisW:我现在的理念是结构应该在实际中尝试模拟不可变类,或者应该是暴露的公共字段的集合,而没有任何写this的方法。 API应该避免编写this的struct方法,因为当这些方法用于只读结构时,编译器会生成伪代码。您提供的API是最有问题的形式;我想你的意思是说point = point.WithX(point.X+3);或Point2d.SetX(ref point, point.x+3);
-
@ChrisW:基本上,正如我所说,暴露字段结构并不真正"封装"任何东西;在封装有用的情况下,这是一件坏事,但在某些情况下封装是一个障碍。如果一个struct不包含一组可以在没有副作用的情况下读取的值,并且如果可以创建一个没有副作用的任何这些值组合的实例,那么这些事实将完全定义结构的语义 - 它将等效于具有这些类型的字段的公开字段结构。那么为什么让人们把它当作它的尴尬呢?
I don't want to complicate reasoning
about this by considering ref
parameters and boxing. I am also aware
that p = p.Offset(3, 4); expresses
immutability much better than
p.Offset(3, 4); does. But the
question remains - aren't value types
immutable by definition?
那么,你真的不是在现实世界中运作,是吗?在实践中,值函数在函数之间移动时复制自身的倾向与不变性很好地融合,但它们实际上并不是不可变的,除非你使它们不可变,因为正如你所指出的,你可以使用对它们的引用像其他任何东西。
-
当然,这是一个非常理论上的问题,而且还有ref和拳击的东西 - 我还没有完全弄明白。我倾向于说ref是没有问题的,因为你获得了对变量的引用,而不是对包含的值。拳击似乎有点困难,我仍然在考虑它。
-
你对ref的论证没有意义。是的,您获得了对它的引用,但您正在修改的值仍然是值类型。
-
我也不太明白你所说的关于ref的内容。这是对象:我已经获得了它的引用。我可以更改它,它会更改与内存中同一个对象关联的值。在这个词的任何意义上,这是如何"不可改变的"?此时它的行为与任何引用类型相同。
-
给定方法static void Bar(ref int arg){arg = 42; }和int foo = 1;酒吧(富);.这将修改foo,但它不应该表明int是可变的。这是因为您获得了对变量foo的引用,而不是包含的int值。
-
@daniel:如果你真的不理解参考文献,那么你就不会理解可变性的真正问题。对低级语言(C)有一点经验,你会发现问题。然后检查一个理智的hilevel语言(Scheme和Lua非常适合这个),你将了解不变性如何帮助。
-
我已经用汇编程序和Prolog编写了多种语言的代码。如果您认为我错过了某些内容,请举例说明我将要添加到原始问题中的问题。
去年我写了一篇关于你可以通过不构造结构而遇到的问题的博客文章
不可改变的。
完整的帖子可以在这里阅读
这是事情可能出现严重错误的一个例子:
1 2 3 4 5 6 7 8
| //Struct declaration:
struct MyStruct
{
public int Value = 0;
public void Update(int i) { Value = i; }
} |
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12
| MyStruct [] list = new MyStruct [5];
for (int i =0;i <5;i ++)
Console .Write(list [i ].Value +"");
Console .WriteLine();
for (int i =0;i <5;i ++)
list [i ].Update(i +1);
for (int i =0;i <5;i ++)
Console .Write(list [i ].Value +"");
Console .WriteLine(); |
此代码的输出是:
现在让我们这样做,但用数组替换泛型List<>:
1 2 3 4 5 6 7 8 9 10 11 12
| List <MyStruct > list = new List <MyStruct >(new MyStruct [5]);
for (int i =0;i <5;i ++)
Console .Write(list [i ].Value +"");
Console .WriteLine();
for (int i =0;i <5;i ++)
list [i ].Update(i +1);
for (int i =0;i <5;i ++)
Console .Write(list [i ].Value +"");
Console .WriteLine(); |
输出是:
解释很简单。不,这不是拳击/拆箱......
从数组访问元素时,运行时将直接获取数组元素,因此Update()方法适用于数组项本身。这意味着更新了数组中的结构本身。
在第二个例子中,我们使用了泛型List<>。当我们访问特定元素时会发生什么?好吧,调用indexer属性,这是一种方法。当方法返回时,值类型总是被复制,所以这正是发生的事情:列表的索引器方法从内部数组中检索结构并将其返回给调用者。因为它涉及值类型,所以将创建一个副本,并且将在副本上调用Update()方法,这当然不会影响列表的原始项目。
换句话说,始终确保您的结构是不可变的,因为您永远不确定何时会创建副本。大多数时候这是显而易见的,但在某些情况下,它真的会让你大吃一惊......
-
问题不在于可变结构是邪恶的,而是C#没有指示哪些方法会改变结构的方法,因此它可以禁止它们在只读上下文中使用。与混杂对象相比,可变结构通常提供非常优越的语义。如果我有一个结构'foo',并且我调用bar1(foo),我可以保证'bar'不会改变foo的任何字段(如果某些字段包含类引用,当然可能是这些参考文献的目标可以改变)。如果我调用bar2(ref foo),那么bar2()可能会改变foo,但是......
-
......任何此类更改将在bar()返回之前发生。 相比之下,如果我有一个类对象'zoo'并且我调用bar3(动物园),bar3()可能会立即改变动物园,或者它可能存储对动物园某处的引用,这会引起其他一些线程在 一些随意的未来时间。 这似乎比任何可变结构的问题都更加邪恶。 可以肯定的是,.net对可变结构的支持有一些奇怪的怪癖,但那些是.net的错误,而不是可变结构的概念。
我认为令人困惑的是,如果你的引用类型应该像值类型一样,那么使它成为不可变的是一个好主意。值类型和引用类型之间的主要区别之一是,通过ref类型上的一个名称进行的更改可以显示在另一个名称中。值类型不会发生这种情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class foo
{
public int x ;
}
public struct bar
{
public int x ;
}
public class MyClass
{
public static void Main ()
{
foo a = new foo ();
bar b = new bar ();
a .x = 1;
b .x = 1;
foo a2 = a ;
bar b2 = b ;
a .x = 2;
b .x = 2;
Console .WriteLine("a2.x == {0}", a2 .x);
Console .WriteLine("b2.x == {0}", b2 .x);
}
} |
生产:
现在,如果你有一个类型,你想要有价值语义,但不想实际使它成为一个值类型 - 也许是因为它需要的存储太多或其他什么,你应该考虑不变性是一部分该设计。使用不可变的ref类型,对现有引用所做的任何更改都会生成一个新对象,而不是更改现有对象,因此您将获得值类型的行为,即您持有的任何值都不能通过其他名称进行更改。
当然,System.String类是此类行为的主要示例。
-
这一点很清楚 - 具有值类型语义的引用类型必须或至少应该设计为不可变的。从您的陈述"[...]这不会发生在值类型中:[...]"我得出结论,您倾向于同意我的结论 - 值类型根据定义是不可变的,因为您无法获取对值的引用, 对?
-
否 - 根据定义,值类型不是不可变的。在上面的示例中,b.x = 2;语句更改b - 它也不会更改b2。这是值类型和ref类型之间的关键区别。我想你可以看一下它,好像b在它被改变的时候得到一个带有新值的全新对象,但这不是正在发生的事情,我认为没有什么有用的东西可以用这种方式思考。
-
现在你明白了我的意思。我目前正在考虑myStruct.DoStuff();重写为myStruct = myStruct.DoStuff();因为这显然使结构的不变性。我的问题可以改写 - 你能找到一个例子,其中提到的转变不能完成或不起作用?
-
我认为没有技术问题。
-
"你能找到一个例子......?"是的,如果b和b2都声明为IBar接口。
-
@DanielBrückner:Threading.Interlocked.Increment(MyStruct.SomeField);之类的操作无法通过MyStruct = something(MyStruct)形式的任何语句进行模拟。但是,它可以通过MyStructType.BumpField(ref MyStruct);进行模拟。
要定义类型是可变的还是不可变的,必须定义"类型"所指的内容。当声明引用类型的存储位置时,声明仅分配空间来保存对存储在别处的对象的引用;声明不会创建有问题的实际对象。尽管如此,在大多数讨论特定引用类型的上下文中,人们不会讨论包含引用的存储位置,而是讨论由该引用标识的对象。事实上,人们可以写入存储位置来保存对象的引用,这意味着对象本身绝不可变。
相反,当声明值类型的存储位置时,系统将在该存储位置内为该值类型所拥有的每个公共或私有字段分配嵌套存储位置。关于值类型的所有内容都保存在该存储位置中。如果定义类型Point的变量foo及其两个字段X和Y,则分别按住3和6。如果将foo中的Point的"实例"定义为该对字段,那么当且仅当foo是可变的时,该实例才是可变的。如果将Point的实例定义为这些字段中保存的值(例如"3,6"),那么这样的实例根据定义是不可变的,因为更改其中一个字段会导致Point保持不同实例。
我认为将值类型"实例"视为字段而不是它们所持有的值更有帮助。根据该定义,存储在可变存储位置中并且存在任何非默认值的任何值类型将始终是可变的,无论它是如何声明的。语句MyPoint = new Point(5,8)构造一个Point的新实例,其字段为X=5和Y=8,然后通过将其字段中的值替换为新创建的Point的值来变异MyPoint。即使结构体无法修改其构造函数之外的任何字段,结构类型也无法保护实例不会使其所有字段都被另一个实例的内容覆盖。
顺便提一下,一个简单的例子,其中一个可变结构可以实现通过其他方式无法实现的语义:假设myPoints[]是一个可供多个线程访问的单元素数组,有二十个线程同时执行代码:
1
| Threading.Interlocked.Increment(myPoints[0].X); |
如果myPoints[0].X开始等于零并且20个线程执行上述代码,无论是否同时,myPoints[0].X将等于20。如果有人试图模仿上述代码:
1
| myPoints [0] = new Point (myPoints [0].X + 1, myPoints [0].Y); |
然后,如果任何线程在另一个线程读取它并回写修改后的值之间读取myPoints[0].X,则增量的结果将丢失(结果是myPoints[0].X可以任意地以1到20之间的任何值结束。
不,根据定义,值类型不是不可变的。
首先,我最好问一个问题"值类型的行为类似于不可变类型吗?"而不是问他们是否是不可变的 - 我认为这引起了很多混乱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct MutableStruct
{
private int state ;
public MutableStruct (int state ) { this.state = state ; }
public void ChangeState () { this.state++; }
}
struct ImmutableStruct
{
private readonly int state ;
public MutableStruct (int state ) { this.state = state ; }
public ImmutableStruct ChangeState ()
{
return new ImmutableStruct (this.state + 1);
}
} |
[未完待续...]
不,他们不是。例:
1 2 3
| Point p = new Point (3, 4);
Point p2 = p ;
p .moveTo (5, 7); |
在此示例中,moveTo()是就地操作。它改变隐藏在参考p后面的结构。您可以通过查看p2来看到它:它的位置也会发生变化。对于不可变结构,moveTo()必须返回一个新结构:
现在,Point是不可变的,当您在代码中的任何位置创建对它的引用时,您将不会有任何意外。我们来看看i:
1 2 3
| int i = 5;
int j = i;
i = 1; |
这是不同的。 i不是不可变的,5是。第二个赋值不会复制对包含i的结构的引用,但会复制i的内容。在幕后,会发生一些完全不同的事情:您获得变量的完整副本,而不是内存中的地址副本(参考)。
与对象等效的是复制构造函数:
1 2
| Point p = new Point (3, 4);
Point p2 = new Point (p ); |
这里,p的内部结构被复制到一个新的对象/结构中,p2将包含对它的引用。但这是一个相当昂贵的操作(与上面的整数赋值不同),这就是为什么大多数编程语言都有区别的原因。
随着计算机变得越来越强大并获得更多内存,这种区别将会消失,因为它会导致大量的错误和问题。在下一代中,只有不可变对象,任何操作都将受到事务的保护,甚至int也将是一个完整的对象。就像垃圾收集一样,它将是程序稳定性的一大进步,在最初几年引起很多悲痛,但它将允许编写可靠的软件。今天,计算机还不够快。
-
你说,"现在,Point是不可变的等等",但这不是一个好例子:Point不是一成不变的。
-
如果Point是值类型,那么在方法调用p.moveTo(5,7)之后p2将不等于p。
-
@Daniel:我是对的,因为在我的例子中,Point不是值类型。 ("就地操作")
-
@ChrisW:因为没有方法可以就地修改它。
-
澄清一塌糊涂:我错过了".net"。我说的是一般的OO语言,特别是.net。
-
"这是不同的。我不是一成不变的,5是。"这是个好的观点。变量本身是可变的,但不是变量的值。所以反对你回答"不,他们不是。"我仍然相信他们是。你能给出一个例子,其中myStruct.DoStuff()不能解释为myStruct = myStruct.DoStuff()吗?
-
当myStruct.DoStuff()更改对象的内部状态时,任何持有对该对象的引用的人都将受到影响。如果对象的所有方法都返回一个新对象,则不能从外部更改此内部状态,使其像"5"一样有效不可变。以同样的方式添加1到5不会改变5。
-
也就是说,我对.Net中的结构一无所知:)所以这就是它的工作原理。
-
.NET中的结构是像int这样的值类型 - 因此您不能引用字段或变量的值。
-
@Daniel:"你不能参考价值",是的,你可以,看看我的答案:)
当对象/结构以无法更改数据的方式传递给函数时,它们是不可变的,并且返回的结构是new结构。经典的例子是
String s ="abc";
s.toLower();
如果写了toLower函数,那么它们会返回一个替换"s"的新字符串,它是不可变的,但是如果函数逐个字母替换"s"中的字母并且从不声明"新字符串",则它是可变的。