Possible Duplicate:
Why are mutable structs evil?
我在很多地方读过它,包括这里,最好使结构不可变。
这背后的原因是什么?我看到许多微软创建的结构是可变的,就像XNA中的结构一样。或许BCL中还有更多。
不遵守这一准则有哪些利弊?
结构应表示值。值不变。数字12是永恒的。
但是,考虑:
1 2 3 4
| Foo foo = new Foo (); // a mutable struct
foo .Bar = 27;
Foo foo2 = foo ;
foo2 .Bar = 55; |
现在foo.bar和foo2.bar是不同的,这通常是意想不到的。尤其是在像属性这样的场景中(幸运的是编译器检测到了这一点)。还有收藏品等,你是如何明智地改变它们的?
对于可变结构,数据丢失太容易了。
- 谢谢贾景晖。速度方面,您认为更改结构中的值比创建新结构更快吗?
- 我怀疑你是否注意到了标准应用程序的区别。可能有一些非常特殊的情况你确实注意到了,但对于一般的指导,不可变的胜诉。
- 不管答案是什么(我不知道),除非存在瓶颈,并且探查器告诉您这是问题的根源,否则永远不要让性能问题胜过好的设计。
- 谢谢你,马克,那我就按这条准则去做。
- 感谢Michael,我之所以谈论速度,是因为对我来说,更改结构的值看起来很明智,可能是因为我使用的其他语言,或者它看起来像类。
- @亨克=那是不同的。我没有重新分配foo2;我更新了它。例如,我不说I.Sign=!i.签名更新i;我用i=-i;重新分配它…重新分配它与更新属性非常不同。
- 我后来的文章(希望你喜欢)是关于如何打破"只读"C编译器的不可变规则:stackoverflow.com/questions/4720475/…
- 我不知道可变结构有多糟糕,如果我修改一个副本,我不希望修改原始结构,这就是我使用类的目的。我同意@henk holterman的评估,我不明白为什么您希望一个结构首先是不可变的。
最大的缺点是,事物的行为并不符合您的期望——特别是当可变性来自于直接值和其中的引用类型的混合时。
老实说,我记不清人们在新闻组中使用可变结构时会遇到什么奇怪的问题,但这些原因确实存在。可变结构导致问题。远离。
编辑:我刚刚找到一封关于这个主题的电子邮件。它只阐述了一点:
这在哲学上是错误的:结构应该代表某种基本价值。这些基本上是不变的。你不能换5号。可以将变量的值从5更改为6,但逻辑上不会对值本身进行更改。
这实际上是个问题:它会造成很多奇怪的情况。如果它是通过接口可变的,那就特别糟糕。然后您可以开始更改装箱值。艾克。我看到过很多新闻组文章,这些文章是由于人们试图使用可变结构并遇到问题。我看到了一个非常奇怪的LINQ示例,它失败了,因为例如,List.Enumerator是一个结构。
- 谢谢乔恩。速度方面,您认为更改结构中的值比创建新结构更快吗?
- @琼·文奇:我会回答:不,或者更确切地说,有一个对构造函数的方法调用,但是它很可能是内联的,除非您的构造函数特别重。
- @伦道夫:谢谢。但是对于构造函数中的.x=2,vs x=x,y=y,z=z这样的情况,它是有效的吗?
- @琼:在极少数情况下,可能会快得多。不过,如果没有充分的理由,我不会为了速度而使用可变结构。
- 是的,更改结构中的值比创建新结构更快。但是,除非您像XNA一样对性能敏感,否则差异无关紧要。
- 谢谢乔恩。你是对的,我并不真正喜欢速度胜过设计,但是当你看到很多这样做的例子时,比如我的开发领域所在的XNA框架等,我只是假设它必须更好、更快等等。
- 谢谢格雷恩沃尔夫。我明白你的观点,但我也非常小心速度,因为写一个非常大的基于3D的应用程序,这是非常计算密集的,所以尽量使事情在设计正确的早,而不是晚。另外,我不赞成坏的实践,但是使用可变结构似乎不…
- 或者在我看来很不自然,语法方面,但是我可以理解你们提到的一些问题。
- 我可以看到XNA是一个例外(我相信已经有了可变结构)-但是要小心,不要说我们没有警告你;)
- 约翰,结构变量和int变量有什么不同?我不反对这个建议,但没有得到"哲学"的论证。
- @Henk;你永远不会改变int;你创造了一个新的int。这就是不变性。
- 你睡过觉吗?XDDD
- 真正的程序员不睡觉,他们只是时不时地放弃剩余的时间。;)
- 同样,将结构作为引用传递呢?这也是骗局吗?我看到的唯一的问题是,它比刚过去慢。
- @琼:按引用传递结构在有意义的地方是可以的。这并不是改变一个特定值中的内容,而是将一个变量的值改变为一个新的值。
我经常在我的(性能关键的)项目中使用可变结构——而且我不会遇到问题,因为我了解复制语义的含义。据我所知,人们提倡不可变结构的主要原因是,不理解其含义的人不会陷入麻烦。
这不是一件可怕的事情,但我们现在正处于它成为"福音真理"的危险之中,事实上,有时它是使结构可变的最佳选择。就像所有事情一样,规则也有例外。
- 请记住,大多数建议来自"框架设计指南"。这意味着他们的目标受众是为其他人编写API的人。在这种情况下,编写no-op"myclass.someValue.approperty=5"的能力只会招致失败。
- @jonathanalen:解决方法是:(1)允许用一个属性来标记属性和方法,该属性指示它们是否应该在只读上下文中的结构上可用;(2)让结构公开字段,而不是自变异属性。我想不出任何一个框架结构,它公开了使用公开字段无法更好地实现的自我转换属性。注意,编译器不必猜测myClass.SomeValue.AField = 5是否会尝试突变myClass.SomeValue。它可以告诉我们它会的。
- @jonathanalen:我真的很恼火,没有办法重写编译器的"猜测",即属性设置器将修改一个结构(在只读上下文中应该是禁止的),或者其他方法不会(因此应该是允许的),特别是在某些情况下,结构包装一个不可变的引用是有用的。对于可变类,并让属性设置器对类项执行操作,但最终必须使用类来创建不必要的GC压力,从而允许属性访问器工作。
您已经询问了不遵循结构应该是不可变的准则的利弊。
缺点:现有的答案很好地涵盖了缺点,并且描述的大多数问题都是由于相同的原因——由于结构的值语义而导致的意外行为。
优点:使用可变结构的主要优点是性能。显然,这个建议附带了所有关于优化的常见警告:确保您的部分代码需要优化,并确保任何更改确实通过分析优化了代码的性能。
有关讨论何时使用可变结构的优秀文章,请参阅RicoMariani的基于值编程的性能测试(或者更具体地说,答案)。
技术上的原因是可变结构似乎能够做他们实际上不做的事情。由于设计时语义与引用类型相同,因此开发人员会感到困惑。此代码:
1 2 3 4
| public void DoSomething(MySomething something)
{
something.Property = 10;
} |
根据MySomething是struct还是class的情况,其行为有很大不同。对我来说,这是一个令人信服的理由,但不是最令人信服的理由。如果您查看ddd的value对象,就可以看到如何处理结构的连接。DDD中的值对象可以最好地表示为.NET中的值类型(因此也是结构)。因为它没有身份,所以不能改变。
从你的住址这样的角度来考虑这个问题。您可以"更改"您的地址,但地址本身没有更改。事实上,你有一个新的地址分配给你。从概念上讲,这是可行的,因为如果你真的改变了地址,你的室友也必须搬家。
没有比可变结构更便宜的操作了,这就是为什么你经常在高性能代码中看到它,比如图形处理例程。
不幸的是,可变结构不能很好地处理对象和属性,修改stuct的副本而不是结构本身太容易了。因此,它们不适用于大多数代码。
P.S.为了避免复制可变结构的成本,通常将它们存储并传递到数组中。
- imho,属性不能很好地与结构一起工作的事实是属性工作方式的错误,并且缺少返回对值类型数组元素引用的任何标准方法。如果系统有一个标准的arrayIndexer(属于t)类型,其中包含对数组的引用和对它的索引,那么值类型属性可以工作得更好。
- 如果不起作用,仍然存在编译器或JIT优化器将整个结构复制到堆栈上的风险。
- 为了让这个想法真正有帮助,编译器和抖动必须知道并尊重arrayIndexer(属于t)类型的语义,它应该提供一种访问数组的适当元素而不暴露数组本身的方法。这可以通过使用accessElement(int index,ref pt1 p1,ref pt2 p2,delegate(ref t item,ref pt1 p1,ref pt2 p2))方法来实现,该方法将接受一个回调函数,该函数应将特定数组项传递给该函数。在没有编译器支持的情况下,代码会很难看,但是在有了支持的情况下,代码就可以了。
结构通常应该表示某种单一的统一。因此,改变一个值的属性没有多大意义,如果你想要一个不同的值,那么创造一个全新的值就更有意义了。
当使用不可变结构时,语义变得更简单,并且您可以避免这样的陷阱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| // a struct
struct Interval {
int From { get; set; }
int To { get; set; }
}
// create a list of structs
List <Interval > intervals = new List <Interval >();
// add a struct to the list
intervals .Add(new Interval ());
// try to set the values of the struct
intervals [0].From = 10;
intervals [0].To = 20; |
结果是列表中的结构根本没有更改。表达式间隔[0]从列表中复制结构的值,然后更改临时值的属性,但该值永远不会放回列表中。
编辑:将示例更改为使用列表而不是数组。
- 在处理遗留代码时,这已经杀了我很多次了!将结构实现为不可变的值对象可以完全避免这种情况。
- 谢谢你,盖法,但你确定吗?Afaik数组是唯一允许这样做的集合。
- 是的,你是对的,我测试过这个,它实际上和一个数组一起工作。我编辑了示例以使用列表。
- 谢谢你,格法,一切都很好:)
- Interval是一个结构,因此不能使用索引器。我想你是指intervals[0],而不是Interval[0]。即使这样,编译器也不允许赋值,因为它不是变量。您需要先分配一个局部变量,如var i = intervals[0];。更改此项目不会更改列表中的项目。
- @布莱恩:是的,区间[0]是个打字错误,谢谢注意。编译器的较新版本可能会做一些额外的检查,并注意到更改属性实际上不会更改任何内容,但早期版本没有。即使编译器在发现此类问题方面做得更好,也总会有一些情况无法向您发出警告。
您应该使结构不可变的原因是它们是值类型,这意味着每次将它们传递给方法时都会复制它们。
因此,例如,如果您有一个返回结构的属性,那么修改该结构上字段的值将是毫无价值的,因为getter将返回该结构的副本,而不是对该结构的引用。我在代码中看到过这种情况,通常很难理解。
如果您设计的结构是不可变的,那么您可以帮助程序员避免这些错误。
当复制周围的结构时,会复制它们的内容,因此如果修改复制的版本,"原始"将不会更新。
这是错误的来源,因为即使您知道自己陷入了复制结构(只是通过将结构传递给方法)和修改副本的陷阱。
上周又发生在我身上,让我找了一个小时的虫子。
保持结构不变可防止…
除此之外,首先需要确保有一个很好的理由来使用结构-"优化"或"我想要在堆栈上快速分配的东西"不算作答案。在您依赖布局的地方进行封送或处理—好的,但是您通常不应该将这些结构保留很长时间,它们是值而不是对象。
- 谢谢,我对结构的主要依赖来自这样一个事实:XNA等框架几乎所有的东西都使用它们,所以即使我需要自己编写,我也只使用结构。