在这里讨论之后,我已经读了好几遍易变结构是"邪恶"的评论(就像这个问题的答案一样)。
C中的可变性和结构的实际问题是什么?
- 声称可变结构是邪恶的,就像声称可变的ints,bools,所有其他的价值类型都是邪恶的。有些情况下是可变的和不变的。这些情况取决于数据所扮演的角色,而不是内存分配/共享的类型。
- @滑动int和bool不可变。
- ….-语法,使使用引用类型数据和值类型数据的操作看起来相同,即使它们明显不同。这是C语言属性的错误,而不是结构的错误——有些语言提供了替代的a[V][X] = 3.14语法来进行适当的转换。在c中,最好提供结构成员的mutator方法,如"mutatev(action
[mutator)",并像a.MutateV((v) => { v.X = 3; })那样使用(由于c对ref关键字的限制,示例被过度简化,但应该有一些解决方法)。]
- @Sushi271解决方法(n):克服程序或系统中的问题或限制的方法。这是C语法中的一个限制。在泛型类型中,ref关键字的理想用法不是含糊不清的,只是不受支持。这是一个限制。是的,C在许多方面迫使不方便的语法来完成这项工作。C的笨拙最经典的例子是EDCOX1,9,遍及每个成员,而不是C++风格的EDCOX1,10的前缀或Ruby风格的独立EDCOX1,11。
- @有许多成员的sushi271结构并不常见,因为复制大量数据成员的效率很低。用例是相同的,但是优秀的程序员为了提高效率会(并且历史上已经)选择不同的方法。所以,是的,对于任何值类型的数据,轻量级通常都更好。
- @滑倒吧,我认为这类结构恰恰相反。为什么您认为已经在.NET库中实现的结构(如日期时间或时间跨度(如此类似的结构))是不可变的?也许只更改这样结构的var的一个成员是有用的,但这太不方便了,会导致太多的问题。实际上,对于处理器计算的内容,您是错的,因为C不编译为汇编程序,所以它编译为IL。在IL中(如果我们已经有了名为x的变量),这个操作是4条指令:ldloc.0(将0-索引变量加载到……
- …类型。T是类型。ref只是一个关键字,它使变量被传递给方法本身,而不是方法的副本。它对引用类型也有意义,因为我们可以更改变量,即方法外部的引用在方法内部更改后将指向其他对象。由于ref T不是类型,而是传递方法参数的方式,因此不能将它放入<>中,因为只能将类型放入其中。所以这是不正确的。也许这样做比较方便,也许C小组可以为一些新版本制作这个,但是现在他们正在研究一些……
- …更重要的是,比如空条件运算符或自动属性初始值设定项。至于public…我真的很喜欢这样做的方法。实际上,重构更容易,因为在一个位置为一个成员删除/添加一个公共对象不会影响所有其他成员。在C++中,经常需要添加两个标签(例如,公共的和私有的:再次),或者将方法移动到类的其他部分。不方便。不管怎样,提到这个话题让我意识到我们应该结束讨论。我们开始为自己的观点争论,我们可以战斗多年,却找不到共同点。
- 地面。每个人都有权发表自己的意见。先生,您似乎更受C++的约束,而我过去8年的主要语言一直是C++。当然,学习别人对某些事情的看法是有教育意义的,然而现在我们偏离了主题。所以谢谢你的意见。
- 我们在2017年,可变结构又回来了!:d就像以前一样,它是关于如何以及何时使用它们的知识。也就是说,我可能会对任何初学者说,"远离结构,或者至少使它们可变——然后在您有更多经验的时候再回到它们,它们提供特殊的好处"。(了解引用是带有特殊运算符重载的特殊地址值类型可以解决所有问题,但对于初学者来说,这太难了。)
- 我同意@anorzaken的观点,只有当你不知道值和引用类型之间的区别时,可变结构才是"邪恶的",否则使用结构是有效的,即使是可变的,只要你知道你在做什么。(btw.net本身有许多可变结构)
结构是值类型,这意味着它们在传递时被复制。
因此,如果您更改了一个副本,那么您只更改了那个副本,而不是原始副本,也不更改可能存在的任何其他副本。
如果您的结构是不可变的,那么由值传递产生的所有自动副本都将是相同的。
如果要更改它,必须有意识地使用修改后的数据创建结构的新实例。(不是复制品)
- "如果你的结构是不可变的,那么所有的副本都是相同的。"不,这意味着如果你想要一个不同的值,你必须有意识地制作一个副本。这意味着你不会因为修改原件而被抓到修改副本。
- @卢卡斯,我想你说的是另一种复制品。我说的是自动复制品,它是通过价值传递而产生的,你的"有意识复制品"是不同的,目的是你没有错误地制作它,它不是真正的复制品。它是一个包含不同数据的有意的新瞬间。
- 你的编辑(16个月后)使这一点更加清晰。不过,我仍然支持"(不变结构)意味着您不会因为修改副本而被抓到,因为您在修改原始副本"。
- @卢卡斯:复制一个结构,修改它,并不知何故地认为一个人正在修改它的原稿(当一个人正在写一个结构字段的事实使自己明显地认为一个人只是在写一个结构的副本的事实)的危险似乎很小,相比之下,某人持有一个类对象作为持有信息的一种手段的危险是C。其中包含的内容将改变对象以更新其自身的信息,并在过程中损坏其他对象所持有的信息。
- 为了避免"邪恶",我们是否需要为我们遇到的每个结构(矩形、大小、点等)制作类包装器?那不是很笨拙吗?
- @VictorPrograss:将结构存储在单个元素数组中,它将像类一样工作。或者,如果定义了一个类public ExposedFieldHolder { public T Value; public ExposedFieldHolder(T v) { Value = v; } },那么就可以使用它将任何结构转换为实体(使类型成为公共的是无害的,因为类型的整体目的是具有完全指定的语义,并且公开它不会将任何实例公开给不应该看到它们的代码)。
- 对于我来说,不可变数据结构的最大优点是,由于它们不能被修改,所以您可以轻松地使多线程分布式工作在一个数据块上,而无需手动检查和处理同步问题(对该数据的跨线程访问)。这本质上使代码比使用可变的数据结构(通过设计,实际上强制任何查看它的线程都不能修改它)更干净、更健壮和无缺陷。还有顺序性的问题。如果两个线程在同一个地方查找数据,那么指向该"副本"的指针自那时起就发生了更改。
- 我不认为有任何约定说结构应该是不可变的。您可以在System.Drawing中找到许多可变结构,例如(point、rectangle,…)。
- 第三段听起来最多是错误的或不清楚的。如果结构是不可变的,那么您将无法修改其字段或任何复制的字段。"如果你想改变它,你必须……这也是误导,你不能改变它,无论是有意识的还是无意识的。创建一个新实例,您想要的数据除了具有相同的数据结构外与原始副本无关。
从哪里开始;-p
埃里克·利珀特的博客总是很适合引用:
This is yet another reason why mutable
value types are evil. Try to always
make value types immutable.
首先,你很容易丢失变化…例如,从列表中删除内容:
1 2
| Foo foo = list[0];
foo.Name ="abc"; |
那改变了什么?没什么有用的…
属性相同:
1
| myObj.SomeProperty.Size = 22; // the compiler spots this one |
强迫你这样做:
1 2 3
| Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar; |
不太关键的是,存在大小问题;可变对象往往具有多个属性;但是,如果您有一个结构具有两个int、一个string、一个DateTime和一个bool,那么您可以很快地烧掉大量内存。对于一个类,多个调用方可以共享对同一实例的引用(引用很小)。
- 好吧,是的,但是编译器就是这样愚蠢。不允许向属性结构成员赋值是一个愚蠢的设计决策,因为它被允许用于++运算符。在这种情况下,编译器只是自己编写显式赋值,而不是让程序员忙碌。
- @konrad:myobj.someproperty.size=22将修改myobj.someproperty的副本。编译器正在将您从一个明显的错误中拯救出来。而且它不允许用于++。
- @卢卡斯:嗯,Mono C编译器当然允许这样做——因为我没有Windows,所以我无法检查微软的编译器。
- @konrad-ms编译器拒绝:`class test public mystruct value get;set;public struct mystruct public int bar get;set;public static void main()test=new test();test.value.bar+;`-这对mono有效吗?
- @马克:实际上我说的是一种间接性:test.Bar++或test.Bar += 1。这也改变了价值——事实上,它或多或少相当于var tmp = test.Bar; tmp += 1; test.Bar = tmp;。所以情况并不完全相同,但这不是我的观点。我的观点是编译器可以并且确实在有意义的地方重写代码。这里产生的重写几乎完全相同于你在答案中提出的手动重写。唯一不同的是bar.Size = 22;和bar = bar + 1。
- @ Marc:Duh。我收回这一点——这里有一个关键的区别,我的代码总是创建一个新的对象,它也适用于不可变的对象。
- @Konrad-少了一个间接的方法,它就可以工作了;它是"改变某个东西的值,这个值只作为堆栈上的一个瞬时值存在,并将蒸发为虚无",这就是被阻塞的情况。
- @MarcGravell:在前一段代码中,您最终得到一个"foo",其名称为"abc",其他属性为列表[0]的属性,而不会干扰列表[0]。如果foo是一个类,则需要克隆它,然后更改副本。在我看来,值类型与类之间的区别最大的问题是使用"."运算符有两个目的。如果我有自己的druers,类可以同时支持"."和"->"的方法和属性,但是"."属性的正常语义是创建一个修改了相应字段的新实例。
- 为什么myObj.SomeProperty.Size =要修改myObj.SomeProperty的副本,而bar.Size = 要修改bar而不是bar的副本?这就是糟糕的决定…
- 我尝试了myobj.someproperty.size=22;它编译并运行良好。
- @倒着说,你可能在比较一个不同的场景,那么,要么SomeProperty实际上不是一个属性(也许它是一个字段?)或者,SomeProperty的类型实际上不是struct的类型。这里有一个显示cs1612:sharplab.io/&hellip;的最小repro。
- @MarcGravell你说得对。谢谢你把它清理干净。一旦您认识到属性只是返回值的方法,那么很明显返回的值是值类型的副本!
- "。如果你有一个结构有两个整数、一个字符串、一个日期时间和一个bool,你可以很快地烧掉大量的内存",这听起来就像一个给定的负数。但是,考虑到"堆"对象和对此类对象的引用的额外开销,只有在"同一对象"被多次/多次"引用"并正确缓存/重用时,才会应用未烧录内存。
我不会说坏话,但易变性通常是程序员为了提供最大限度的功能而过度老化的一个标志。事实上,这通常是不需要的,而这反过来又使接口更小、更容易使用和更难使用错误(=更健壮)。
其中一个例子是在竞争条件下的读/写和写/写冲突。因为写操作不是有效的操作,所以不能在不可变的结构中发生。
另外,我声称实际上几乎不需要易变性,程序员只是认为它可能在未来。例如,改变一个日期是没有意义的。相反,创建一个基于旧日期的新日期。这是一个便宜的操作,所以性能不是一个考虑因素。
- 埃里克·利珀特说他们…看看我的答案。
- 不变性当然很好地处理线程,但是您可以编写一个不变性类,同样简单,也同样有用。但仍然是一个很好的答案。我愿意加1,但今天我出去了。
- 虽然我很尊重埃里克·利珀特,但他不是上帝(至少现在不是)。当然,你链接到的博客文章和上面的文章是使结构不可变的合理论据,但实际上,它们作为从不使用可变结构的论据是非常弱的。然而,这篇文章是A+1。
- 在C中开发时,您通常需要不时地改变—尤其是在您的业务模型中,您希望流式处理等能够与现有解决方案顺利工作。我写了一篇关于如何处理可变和不变数据的文章,解决了大多数关于可变的问题(我希望):rickyhelgesson.wordpress.com/2012/07/17/&hellip;
- @Rickyhelgesson:我认为作为设计的一部分,包括可变和不可变的业务对象是一个常见的"可读对象"接口,这一点很有用。既不可变也不持久化其参数的方法应该能够与可变或不可变对象互换操作。
- @stephenmartin:封装单个值的结构通常应该是不可变的,但结构是迄今为止封装固定的独立但相关变量集(如点的x和y坐标)的最佳媒介,这些变量集作为一个组没有"标识"。用于此目的的结构通常应将其变量公开为公共字段。我会认为,对于这样的目的,使用类比使用结构更合适的概念是完全错误的。不可变类通常效率较低,可变类通常具有糟糕的语义。
- @stephenmartin:例如,考虑一个方法或属性,它应该返回图形转换的六个float组件。如果这样的方法返回一个包含六个组件的公开字段结构,那么很明显,修改结构的字段不会修改从中接收它的图形对象。如果这样的方法返回一个可变的类对象,那么更改它的属性可能会更改底层的图形对象,也许它不会——没有人真正知道。
- @超级卫星:我看不出能解决什么问题。对象不可变访问的重要性在于,您知道读取之间是一致的,而且对象的所有属性在任何时候都明显是一致的。可读接口允许您从可能不稳定的对象中读取数据,如果您的属性在单个操作中无法更新,则可能会导致读取危险,从而使接口变得危险。所以,如果我再补充一点,那就必须解决一个非常重要的问题,引入这些危险。
- @Rickyhelgesson:需要不可变对象的代码应该需要可变对象。"可读"接口将用于希望读取可能可变对象的当前状态的代码。除此之外,如果可变类型的构造函数愿意接受可变对象,那么在许多情况下,如果给它一个包含所需信息但不可变的对象,就没有理由不高兴了。定义"可读"接口将避免为可变或不可变的源操作数重载可变对象构造函数。
- 为什么不可变?我有EDOCX1[1]属性,可以是我的屏幕大小,我想在课堂之外更改它。
- @艾哈迈德,我在我的帖子里已经给出了这个理由,而且这个理由是有关联的。
可变结构不是邪恶的。
它们在高性能环境下是绝对必要的。例如,当缓存线和/或垃圾收集成为瓶颈时。
在这些完全有效的用例中,我不会将不可变结构的使用称为"邪恶"。
我可以看出,C的语法对区分值类型或引用类型的成员的访问没有帮助,所以我完全赞成使用强制不可变的不可变结构,而不是可变结构。
然而,我建议不要简单地将不可变结构标记为"邪恶",而应采用这种语言,并提倡更有益和更具建设性的拇指规则。
例如:"结构是默认复制的值类型。如果你不想复制它们,你需要一个证明人"或者"首先尝试使用只读结构"。
- 我还假设,如果一个人想用管道胶带将一组固定的变量固定在一起,这样它们的值可以单独处理或存储,也可以作为一个单元存储,那么让编译器将一组固定的变量固定在一起(即用公共字段声明一个struct)比定义一个可以是u的类更有意义。为了达到相同的目的,或者在结构中添加一堆垃圾,使它模拟这样一个类(而不是让它表现得像一组变量与管道胶带粘在一起,这才是我们真正想要的)。
具有公共可变字段或属性的结构不是邪恶的。好的。
结构方法(与属性设置器不同)会改变"this",这有点邪恶,只是因为.NET不提供将它们与不提供的方法区分开来的方法。不改变"this"的结构方法即使在只读结构上也应该可以调用,而不需要进行任何防御性复制。不应在只读结构上调用实现"this"变异的方法。由于.NET不希望禁止对只读结构调用不修改"this"的结构方法,但不希望对只读结构进行转换,因此它在只读上下文中防御性地复制结构,可以说这是两个领域中最糟糕的一个。好的。
尽管在只读上下文中处理自变异方法存在问题,但是可变结构通常提供远远优于可变类类型的语义。考虑以下三种方法签名:好的。
1 2 3 4 5 6
| struct PointyStruct {public int x,y,z;};
class PointyClass {public int x,y,z;};
void Method1(PointyStruct foo);
void Method2(ref PointyStruct foo);
void Method3(PointyClass foo); |
对于每种方法,回答以下问题:好的。
假设该方法不使用任何"不安全"代码,它会修改foo吗?
如果在调用方法之前不存在对"foo"的外部引用,那么在该方法之后是否存在外部引用?答案:好的。
问题1:&emsp;Method1():否(明确意图)&emsp;Method2()是(明确意图)&emsp;Method3()是(意图不确定)问题2:&emsp;Method1()否&emsp;Method2()否(除非不安全)&emsp;Method3()是好的。< /块引用>
方法1无法修改foo,并且从未获取引用。方法2获取一个对foo的短期引用,它可以使用修改foo的字段任意次数、任意顺序,直到返回,但它不能持久化该引用。在method2返回之前,除非它使用不安全的代码,否则可能由其"foo"引用生成的所有副本都将消失。方法3与方法2不同,它获取了一个可以随意共享的对foo的引用,并且不知道它会用它做什么。它可能根本不会更改foo,可能会更改foo然后返回,或者它可能会将foo引用给另一个线程,该线程可能在将来某个任意时间以某种任意方式改变foo。限制method3对传递给它的可变类对象所做的操作的唯一方法是将可变对象封装到一个只读包装器中,这个包装器既难看又麻烦。好的。
结构数组提供了出色的语义。对于矩形类型的矩形[500],如何将元素123复制到元素456,然后在不干扰元素456的情况下,稍后将元素123的宽度设置为555,这是显而易见的。"矩形[432]=矩形[321];矩形[123];宽度=555;"。知道矩形是一个结构,它有一个称为width的整型字段,这将告诉我们所有人都需要知道上面的语句。好的。
现在假设rectclass是一个具有与rectangle相同字段的类,并且其中一个类希望对rectclassarray[500]类型的rectclass执行相同的操作。也许数组应该包含500个对可变RectClass对象的预初始化不可变引用。在这种情况下,正确的代码应该是"rectclassarray[321].setbounds(rectclassarray[456]);…;rectclassarray[321].x=555;"。可能假定数组包含不会更改的实例,因此正确的代码更像是"rectclass array[321]=rectclass array[456]…";rectclass array[321]=new rectclass(rectclass array[321]);rectclass array[321].x=555;"要知道应该做什么,必须对rectclass(例如它是否支持复制构造函数、复制自方法等)以及数组的预期用途。没有比使用结构更干净的了。好的。
当然,不幸的是,除了数组之外,没有更好的方法可以让任何容器类提供结构数组的干净语义。如果希望用字符串等对集合进行索引,最好的方法可能是提供一个通用的"actonitem"方法,该方法将接受索引的字符串、通用参数以及通过引用通用参数和集合项传递的委托。这将允许与结构数组几乎相同的语义,但除非可以追求vb.net和c_people来提供一个良好的语法,否则代码将是笨拙的,即使它具有合理的性能(传递一个通用参数将允许使用静态委托,并避免创建任何临时类实例)。好的。
就我个人而言,我对埃里克·利珀特等人的仇恨感到恼火。有关可变值类型的spew。它们提供了比各地使用的混乱引用类型更清晰的语义。尽管.NET对值类型的支持有一些限制,但在许多情况下,可变值类型比任何其他类型的实体都更适合。好的。好啊。
- @罗恩·沃霍尔:一个矩形并不是很明显。它可以是其他类型,可以从矩形隐式类型转换。尽管系统定义的类型只能从矩形隐式类型转换为矩形,如果试图将矩形的字段传递给矩形的构造函数(因为前者是单的,而后者是整数),编译器将发出尖叫声,但可能存在允许此类隐式类型转换的用户定义结构。顺便说一句,第一个语句无论是矩形还是矩形都同样有效。
- 你所展示的只是,在一个做作的例子中,你相信有一种方法更清晰。如果我们以你的Rectangle为例,我可以很容易地想出一个你行为非常不清楚的常见情况。假设winforms实现了一个可变的Rectangle类型,该类型在表单的Bounds属性中使用。如果我想更改边界,我想使用您的好语法:form.Bounds.X = 10;,但是这在表单上完全没有改变(并生成一个可爱的错误通知您这样做)。不一致性是编程的祸根,也是需要不可变性的原因。
- 第二点,我看不出这有多重要。用稍微少一点的更改来替换Rectangle和RectangleF,这几乎不是一种常见的情况,而且更需要在任何声明站点更改声明的类型。我认为这是一个延伸,声称能够稍微修改一个类型更容易是值得失去清晰显示在这里的答案。
- @罗恩·沃霍尔:像"form.bounds.x=10"这样的代码无法编译,这一事实很明显它不会改变任何东西。我对矩形的观点是,了解第二个语句的效果所需的信息量远远大于第一个语句的效果。如果你看到代码"myrect=new rectangle(myrect.left+10,myrect.right,myrect.width,myrect.height);"你能肯定地说你能正确识别它的作用吗?另外,假设矩形是一个类。您希望"someform.bounds.x=10;"做什么?为什么?
- @罗恩·沃霍尔:顺便说一句,我想说一句"form.bounds.x=10",让它正常工作,但系统并没有提供任何干净的方法。与使用类的任何方法相比,将值类型属性公开为接受回调的方法的约定可以提供更干净、高效和可确认的正确代码。
- 让我们在聊天中继续讨论
- 这个答案比一些最受欢迎的答案更有洞察力。反对可变值类型的论点依赖于混叠和变异时发生的"你所期望的"这一概念,这有点荒谬。无论如何,这是一件可怕的事情!
- @埃蒙纳波恩:我没有想到这个术语,但你是对的,这与为什么暴露的场结构通常是正确的方法有关:它们只支持短暂的别名。太糟糕了.NET语言没有一个好的模式来显示短暂的别名,不过,我希望有这样一个模式:"foo.bar(57.x=boz;"可以自动转换为"foo.access ou bar(57,(ref point i t,ref int p1)=>i t.x=p1,ref boz)",从而允许除数组之外的集合访问其中包含的结构的内部。
- @超级卫星:谁知道呢,也许他们为C 7所说的参考返回特性可能涵盖了这个基础(我实际上没有详细研究过,但表面上听起来很相似)。
- @eamonnerbonne:目的可能是相似的,但是imho一个合适的访问协议应该让集合知道什么时候访问一个对象,什么时候完成它。从实现的角度来看,"ref-return"后面跟着一个try-finally保护的第二个方法调用是一个很好的方法,但我认为没有任何计划。
- @supercat查看您是否要用7.2更新此答案:blogs.msdn.microsoft.com/seteplia/2018/03/07/&hellip;
值类型基本上表示不变的概念。如果有一个数学值,比如一个整数、向量等,然后能够对其进行修改,这是没有意义的。这就像重新定义一个值的含义。与其更改值类型,不如指定另一个唯一值。请考虑这样一个事实,即通过比较其属性的所有值来比较值类型。关键是,如果属性相同,那么它就是该值的相同通用表示。
正如Konrad提到的,更改日期也没有意义,因为值表示唯一的时间点,而不是具有任何状态或上下文依赖性的时间对象的实例。
希望这对你有意义。当然,这更多的是关于您试图用值类型捕获的概念,而不是实际的细节。
- 嗯,它们至少应该表示不变的概念;-p
- 是的,但我想您可以滥用大多数编程构造
- 好吧,我想他们可以使system.drawing.point不可变,但这是一个严重的设计错误。我认为点实际上是一个典型的值类型,它们是可变的。除了真正的早期编程101初学者之外,它们不会给任何人带来任何问题。
- 原则上,我认为点也应该是不可变的,但是如果它使类型更难使用或更不优雅,那么当然也必须考虑这一点。如果没有人愿意使用,那么让代码构造支持最好的原则是没有意义的;)
- 值类型对于表示简单的不可变概念很有用,但暴露的字段结构是保存或传递相关但独立的小固定值集(例如点的坐标)的最佳类型。这种值类型的存储位置封装其字段的值,而不封装其他值。相反,可变引用类型的存储位置可用于保存可变对象的状态,但也封装了存在于同一对象的整个宇宙中所有其他引用的标识。
- @超级卫星,这是最好的意见。结构只是一堆值。当你开始这样看的时候,任何角落的箱子都不会抓住你。
- @伊利丹:没错。结构对简单聚合以外的事物有用,而假装为低成本对象的结构应表现为对象(通常意味着它们必须是不可变的),但对于简单用作聚合的结构,结构越开放,就越容易有人看到它只不过是一个聚合。。
- "值类型基本上代表不变的概念"。不,它们不是。值类型变量最古老和最有用的应用之一是int迭代器,如果它是不可变的,它将完全无用。我认为您将"值类型"编译器/运行时实现与"类型化为值类型的变量"混为一谈——后者对于任何可能的值都是可变的。
- 根据您在这个答案中陈述的逻辑,所有类型都是不可变的。类存储为值类型和引用(内存地址指针/句柄)的集合,因此它们也是不可变的,因为您不更改内存地址,只需"分配另一个唯一值"。Q显然是关于结构类别数据结构的建议使用,从高级程序员的角度来看,它改变了初始化后一次包含的值和内存位置。将讨论切换到编译器优化使这变得不相关。
从程序员的角度来看,还有另外两个可能导致不可预测行为的角落案例。这两个。
不可变值类型和只读字段
ZZU1〔0〕
可变值类型和数组
假设有一个可变结构的数组,我们正在为该数组的第一个元素调用incrementi方法。你对这个电话有什么期待?它应该更改数组的值还是只更改一个副本?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Mutable [] arrayOfMutables = new Mutable [1];
arrayOfMutables [0] = new Mutable (5);
// Now we actually accessing reference to the first element
// without making any additional copy
arrayOfMutables [0].IncrementI();
//Prints 6!!
Console .WriteLine(arrayOfMutables [0].I);
// Every array implements IList<T> interface
IList <Mutable > listOfMutables = arrayOfMutables ;
// But accessing values through this interface lead
// to different behavior: IList indexer returns a copy
// instead of an managed reference
listOfMutables [0].IncrementI(); // Should change I to 7
// Nope! we still have 6, because previous line of code
// mutate a copy instead of a list value
Console .WriteLine(listOfMutables [0].I); |
所以,只要你和团队的其他成员清楚地了解你在做什么,可变结构就不是邪恶的。但是,当程序行为与预期行为不同时,会出现太多的角情况,这可能导致细微的难以产生和难以理解的错误。
- 如果.NET语言的值类型支持稍好一点,则应该发生的情况是结构方法应该被禁止改变"this",除非它们被显式声明为这样做,并且这样声明的方法应该在只读上下文中被禁止。可变结构数组提供了有用的语义,但不能通过其他方式有效地实现。
- 这些都是很好的例子,非常微妙的问题,将产生于可变结构。我不会料到会有这种行为。为什么数组会给你一个引用,而接口会给你一个值?我想,除了一直以来的值(这是我真正期望的),它至少是另一种方式:提供引用的接口;提供值的数组…
- @Sahuagin:不幸的是,没有标准机制可以让接口公开引用。有一些方法.NET可以安全有效地执行这些操作(例如,通过定义一个包含T[]和整数索引的特殊"arrayref"结构,并提供对ArrayRef类型的属性的访问将被解释为对适当数组元素的访问)[如果一个类想要公开ArrayRef类型的属性]对于任何其他目的,它都可以提供一个方法(而不是属性)来检索它。不幸的是,没有这样的规定。
- 哦,我的…这让易变的结构变得邪恶!
- 当您将转换方法重构为需要ref参数的静态方法:public static void IncrementI(ref Mutable m) { m.I++; }时,编译器应该在大多数时候阻止您执行"错误"的操作。
- 我喜欢这个答案,因为它包含了非常有价值的、不明显的信息。但实际上,这并不是一个反对可变结构的论点。是的,正如埃里克所说,我们在这里看到的是一个"绝望坑",但这种绝望的根源并不是易变的。绝望的根源是结构的自变异方法。(至于数组和列表行为不同的原因,这是因为一个基本上是一个计算内存地址的运算符,另一个是一个属性。一般来说,当您了解"引用"是地址值时,一切都会变得清晰。)
如果您曾经使用类似C/C++的语言编程,Struts很好地被用作可变的。只要把球传给裁判就行了,没有什么会出错的。我发现的唯一问题是C编译器的限制,在某些情况下,我无法强制愚蠢的事情使用对结构的引用,而不是副本(就像当结构是C类的一部分时)。
因此,可变结构不是邪恶的,C使它们邪恶。我使用C++中的可变结构,它们非常方便直观。相反,C使我完全放弃了作为类成员的结构,因为它们处理对象的方式。他们的便利使我们付出了代价。
- 拥有结构类型的类字段通常是一种非常有用的模式,尽管诚然存在一些限制。如果使用属性而不是字段,或者使用readonly,那么性能会降低,但是如果避免这样做,那么结构类型的类字段就很好了。结构的唯一基本限制是,像int[]这样的可变类类型的结构字段可以封装标识或一组不变的值,但不能用于封装可变值而不封装不需要的标识。
假设您有一个由1000000个结构组成的数组。每一个结构代表一个股票,其内容包括出价、出价(可能是小数)等,这是由C/VB创建的。
假设数组是在非托管堆中分配的内存块中创建的,这样其他一些本机代码线程就可以并发地访问数组(可能是一些执行数学运算的高性能代码)。
假设C/VB代码正在监听价格变化的市场反馈,该代码可能需要访问数组的某些元素(对于任何一种安全性),然后修改一些价格字段。
想象一下,这是每秒数万次甚至数十万次。
好吧,让我们面对事实,在这种情况下,我们真的希望这些结构是可变的,它们需要是因为它们被其他一些本机代码共享,所以创建副本不会有帮助;它们需要是因为以这种速度复制大约120字节的结构是愚蠢的,特别是当更新可能只影响一两个字节时。
雨果
- 是的,但在这种情况下,使用结构的原因是这样做是由外部约束(本地代码的使用)强加给应用程序设计的。关于这些对象,您描述的所有其他内容都表明它们应该是C或VB.NET中的类。
- 我不知道为什么有些人认为这些东西应该是类对象。如果所有数组槽都填充了引用不同的实例,则使用类类型将为内存需求添加额外的12或24个字节,并且对类对象引用数组的顺序访问比对结构数组的顺序访问慢得多。
如果你坚持使用什么结构(在C语言中,Visual Basic 6,PASCAL/Delphi,C++结构体类型(或类)当它们不被用作指针)时,你会发现一个结构不只是一个复合变量。这意味着:您将把它们作为一组压缩的变量,用一个公共名称(引用成员的记录变量)来处理。
我知道这会让很多习惯OOP的人感到困惑,但如果使用得当,这还不足以说明这些东西本质上是邪恶的。有些结构按照他们的意愿是不可变的(这是Python的EDCOX1〔0〕的情况),但它是另一个需要考虑的范例。
是的:结构涉及大量的内存,但这样做并不能精确地增加内存:
相比:
1
| point = Point(point.x + 1, point.y) |
在不可变的情况下,内存消耗至少是相同的,甚至更多(尽管对于当前堆栈,这种情况是临时的,取决于语言)。
但最后,结构是结构,而不是物体。在poo中,对象的主要属性是它们的标识,大多数情况下,标识不超过其内存地址。struct代表数据结构(不是适当的对象,因此它们无论如何都没有标识),可以修改数据。在其他语言中,record(而不是struct,正如pascal的情况一样)是一个单词,具有相同的用途:只是一个数据记录变量,用于从文件中读取、修改和转储到文件中(这是主要用途,在许多语言中,您甚至可以在记录中定义数据对齐,但prop不一定如此erly调用了对象)。
想要一个好的例子吗?结构用于轻松读取文件。Python有这个库,因为它是面向对象的,不支持结构,所以必须以另一种方式实现它,这有点难看。实现结构的语言具有此功能…内置的。尝试用Pascal或C等语言读取具有适当结构的位图头。这很容易(如果结构正确构建和对齐;在Pascal中,您不会使用基于记录的访问,而是使用函数读取任意二进制数据)。因此,对于文件和直接(本地)内存访问,结构比对象更好。至于今天,我们已经习惯了JSON和XML,因此我们忘记了使用二进制文件(作为副作用,也忘记了使用结构)。但是是的:它们存在,并且有目的。
他们不是邪恶的。只是为了正确的目的使用它们。
如果你从锤子的角度来思考,你会想把螺丝钉当作钉子来对待,发现螺丝钉更难插入墙中,这将是螺丝钉的错,他们将是邪恶的。
当某物发生变异时,它会获得一种认同感。
1 2 3 4 5 6 7 8
| struct Person {
public string name ; // mutable
public Point position = new Point (0, 0); // mutable
public Person (string name, Point position ) { ... }
}
Person eric = new Person ("Eric Lippert", new Point (4, 2)); |
因为Person是可变的,所以考虑改变埃里克的位置比克隆埃里克、移动克隆和破坏原始位置更自然。两种操作都可以成功地改变eric.position的内容,但一种操作比另一种操作更直观。同样,将Eric传递(作为参考)以获取修改他的方法更为直观。给一个方法克隆一个Eric几乎总是会令人惊讶的。任何想突变Person的人都必须记得向Person求助,否则他们会做错事。
如果您使类型不可变,问题就消失了;如果我不能修改eric,那么我是否接收eric或eric的克隆对我没有任何影响。更一般地说,如果一个类型的所有可观察状态都保存在以下成员中,则该类型可以安全地传递值:
如果满足这些条件,则可变值类型的行为类似于引用类型,因为浅拷贝仍允许接收器修改原始数据。
一个不变的Person的直观性取决于你想做什么。如果Person只是代表一组关于一个人的数据,那么它没有什么不合理的地方;Person变量真正代表抽象值,而不是对象。(在这种情况下,将其重命名为PersonData可能更合适。)如果Person实际上是在建模一个人本身,那么不断创建和移动克隆的想法是愚蠢的,即使你已经避免了认为你在修改原始版本的陷阱。在这种情况下,简单地将Person设置为引用类型(即类)可能更为自然。
当然,正如函数式编程告诉我们的那样,使所有东西都不变是有好处的(没有人能秘密地保持对eric的引用并使其变异),但由于OOP中这不是惯用的,所以对于其他使用您的代码的人来说,这仍然是不可靠的。
- 你关于身份的观点是一个很好的观点;值得注意的是,只有当存在多个对某个事物的引用时,身份才是相关的。如果foo拥有宇宙中任何地方对其目标的唯一引用,并且没有任何东西捕获到该对象的标识散列值,那么变化的字段foo.X在语义上等同于使foo指向一个新对象,该对象与它以前提到的对象相同,但X持有所需的值。UE。对于类类型,通常很难知道是否存在对某个对象的多个引用,但是对于结构,这很容易:它们不存在。
- 如果Thing是一个可变的类类型,那么Thing[]将封装对象标识——不管是否需要它——除非可以确保数组中存在任何外部引用的Thing都不会发生变化。如果不希望数组元素封装标识,通常必须确保它所保存引用的任何项都不会发生变化,或者确保它所保存的任何项都不会存在外部引用[混合方法也可以工作]。两种方法都不太方便。如果Thing是一个结构,那么Thing[]只封装值。
- 对于对象,它们的身份来自它们的位置。引用类型的实例由于其在内存中的位置而具有标识,您只传递它们的标识(引用),而不是它们的数据,而值类型的标识则位于它们存储的外部。您的eric值类型的标识仅来自存储它的变量。如果你把他传出去,他就会失去身份。
埃里克·利珀特先生的例子有几个问题。它旨在说明结构被复制的观点,以及如果不小心的话,这是如何成为一个问题的。看看这个例子,我认为它是一个糟糕的编程习惯的结果,而不是结构或类的真正问题。
结构应该只有公共成员,不需要任何封装。如果是这样,那么它实际上应该是一个类型/类。你真的不需要两个结构来表达同一件事。
如果类包含一个结构,您将调用类中的一个方法来改变成员结构。这是我作为一个好的编程习惯所要做的。
正确的实现方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| struct Mutable {
public int x ;
}
class Test {
private Mutable m = new Mutable ();
public int mutate ()
{
m .x = m .x + 1;
return m .x;
}
}
static void Main (string[] args ) {
Test t = new Test ();
System.Console.WriteLine(t .mutate());
System.Console.WriteLine(t .mutate());
System.Console.WriteLine(t .mutate());
} |
它看起来像是编程习惯的问题,而不是结构本身的问题。结构应该是可变的,这就是思想和意图。
Voila的变化结果与预期一致:
一二三按任意键继续。…
- 设计小的不透明结构,使其行为类似于不可变的类对象,这并没有什么错;当试图使某些行为类似于对象时,msdn指导原则是合理的。结构在某些情况下是合适的,在这种情况下,人们需要一些轻量级的东西,比如物体,在这种情况下,人们需要用管道胶带把一堆变量粘在一起。然而,由于某种原因,许多人没有意识到结构有两种不同的用途,适合其中一种的指导方针不适合另一种。
它与结构没有任何关系(也不与C语言相关),但是在爪哇中,当它们是哈希映射中的键时,可能会遇到可变对象的问题。如果您在将它们添加到地图后更改它们,并且它更改了散列代码,那么可能会发生邪恶的事情。
- 在C中也是如此,所以说得好,是的。
- 如果您也使用类作为映射中的键,这是正确的。
就我个人而言,当我看代码时,以下内容对我来说相当笨拙:
data.value.set(data.value.get()+1);
而不是简单的
data.value++;或data.value=data.value+1;
在传递类时,数据封装很有用,您希望确保以受控方式修改值。然而,当您拥有一个公共集合和一些函数,它们的作用仅仅是将值设置为曾经传入的值时,与简单地传递一个公共数据结构相比,这是一个怎样的改进呢?
当我在一个类中创建一个私有结构时,我创建了这个结构来将一组变量组织成一个组。我希望能够在类范围内修改该结构,而不是获取该结构的副本并创建新的实例。
对我来说,这会阻止有效地使用用于组织公共变量的结构,如果我想要访问控制,我会使用一个类。
- 直奔主题!结构是没有访问控制限制的组织单位!不幸的是,C使它们在这方面毫无用处!
- 这完全忽略了这一点,因为您的两个示例都显示了可变结构。
- C使其不能用于此目的,因为这不是结构的目的。
可变数据有许多优点和缺点。百万美元的劣势是化名。如果同一个值在多个地方被使用,并且其中一个地方更改了它,那么它将神奇地更改为其他使用它的地方。这与比赛条件有关,但不完全相同。
百万美元的优势有时是模块化的。可变状态允许您隐藏不需要知道的代码中的更改信息。
口译员的艺术在某些细节上涉及到这些权衡,并给出了一些例子。
- 结构在C中没有别名。每个结构赋值都是一个副本。
- @递归:在某些情况下,这是可变结构的一个主要优势,这让我质疑结构不应该是可变的这一概念。编译器有时隐式地复制结构这一事实并不能降低可变结构的有用性。
如果使用得当,我不相信它们是邪恶的。我不会把它放在我的生产代码中,但我会做一些类似于结构化单元测试模拟的事情,在这种情况下,结构的寿命相对较小。
使用Eric示例,您可能希望创建该Eric的第二个实例,但要进行调整,因为这是测试的本质(即复制,然后修改)。如果我们只是在测试脚本的其余部分使用eric2,那么对于第一个eric实例会发生什么并不重要,除非您打算将其用作测试比较。
这对于测试或修改Shallow定义特定对象(结构点)的遗留代码非常有用,但是通过使用不可变的结构,这可以防止使用它。
- 如我所见,结构的核心是一堆变量,它们用管道胶带粘在一起。在.NET中,结构可以假装成一堆变量以外的东西,这些变量与管道胶带粘在一起,我建议在实际应用中,一个将要假装成一堆变量以外的东西的类型与管道胶带粘在一起时,应表现为一个统一的对象(对于结构来说,这意味着不变性但是有时候用管道胶带把一堆变量粘在一起是很有用的。即使在生产代码中,我也会认为最好有一个类型…
- …显然,除了"每个字段包含写入它的最后一件事"之外,它没有任何语义,将所有语义推送到使用该结构的代码中,而不是试图让一个结构做更多的工作。例如,如果一个Range类型的成员为Minimum和Maximum字段,并且代码为Range myRange = foo.getRange();,那么关于Minimum和Maximum包含的内容的任何保证都应该来自foo.GetRange();。让EDOCX1[8]成为一个公开的字段结构将清楚地表明它不会添加任何自己的行为。