为啥可变结构是“邪恶的”?
Posted
技术标签:
【中文标题】为啥可变结构是“邪恶的”?【英文标题】:Why are mutable structs “evil”?为什么可变结构是“邪恶的”? 【发布时间】:2010-10-01 06:38:04 【问题描述】:在关于 SO 的讨论之后,我已经多次阅读了可变结构是“邪恶”的评论(就像在question 的答案中一样)。
C# 中的可变性和结构的实际问题是什么?
【问题讨论】:
声称可变结构是邪恶的,就像声称可变的int
s、bool
s 和所有其他值类型都是邪恶的一样。存在可变性和不变性的情况。这些情况取决于数据所扮演的角色,而不是内存分配/共享的类型。
@slipp int
和 bool
是不可变的..
….
-syntax,即使它们明显不同,使用 ref 类型数据和值类型数据的操作看起来也一样。这是 C# 属性的错误,而不是结构的错误——某些语言提供了替代的 a[V][X] = 3.14
语法来进行就地变异。在 C# 中,您最好提供 struct-member mutator 方法,如 'MutateV(Action mutator)` 并像 a.MutateV((v) => v.X = 3; )
一样使用它(由于 C# 的限制,示例过于简化ref
关键字,但应该有一些解决方法).
@Slipp 好吧,我对这类结构的看法完全相反。为什么您认为已经在 .NET 库中实现的结构,例如 DateTime 或 TimeSpan(如此相似)是不可变的?也许只改变这种结构的 var 的一个成员可能会很有用,但这太不方便了,会导致太多问题。实际上,您对处理器计算的内容是错误的,因为 C# 不会编译为汇编程序,而是编译为 IL。在 IL 中(假设我们已经有了名为 x
的变量)这个单一操作是 4 条指令:ldloc.0
(将 0-index 变量加载到...
... 类型。 T
是类型。 Ref 只是一个关键字,它使变量被传递给方法本身,而不是它的副本。它对引用类型也有意义,因为我们可以更改变量,即方法外的引用在方法内更改后将指向其他对象。由于ref T
不是类型,而是传递方法参数的方式,所以不能将其放入<>
,因为只能将类型放入其中。所以这是不正确的。也许这样做会很方便,也许 C# 团队可以为一些新版本制作这个,但现在他们正在做一些......
【参考方案1】:
结构是值类型,这意味着它们在传递时会被复制。
因此,如果您更改副本,则仅更改该副本,而不是原始副本,也不会更改可能存在的任何其他副本。
如果你的结构是不可变的,那么所有由值传递产生的自动副本将是相同的。
如果你想改变它,你必须有意识地通过使用修改后的数据创建一个新的结构实例来做到这一点。 (不是副本)
【讨论】:
"如果你的结构是不可变的,那么所有的副本都是一样的。"不,这意味着如果你想要不同的价值,你必须有意识地复制。这意味着您不会被认为是在修改原件而被抓到修改副本。 @Lucas 我认为您在谈论另一种副本不要错误地制作它,它并不是真正的副本,而是包含不同数据的故意新瞬间。 您的编辑(16 个月后)使这一点更加清晰。不过,我仍然坚持“(不可变结构)意味着您不会因为认为您正在修改原始文件而修改副本”。 @Lucas: 复制一个结构体、修改它以及以某种方式认为自己正在修改原始结构的危险(当一个人正在编写一个结构体字段这一事实使 不言自明时) 一个人只写自己的副本的事实)与持有类对象作为保存其中包含的信息的手段的人将改变对象以更新其自己的信息并在此过程中的危险相比似乎很小破坏其他对象持有的信息。 第 3 段听起来是错误的,或者充其量是不清楚的。如果您的结构是不可变的,那么您将无法修改其字段或任何副本的字段。 “如果你想改变它,你必须......”这也是误导,你不能改变它 永远,无论是有意识的还是不知不觉。创建一个新实例,您想要的数据与原始副本无关,除了具有相同的数据结构。【参考方案2】:从哪里开始 ;-p
Eric Lippert's blog 总是适合引用:
这是可变的另一个原因 值类型是邪恶的。尝试总是 使值类型不可变。
首先,您往往很容易丢失更改...例如,从列表中取出内容:
Foo foo = list[0];
foo.Name = "abc";
这有什么变化?没什么用...
属性也一样:
myObj.SomeProperty.Size = 22; // the compiler spots this one
强迫你这样做:
Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;
不太重要的是,存在尺寸问题;可变对象倾向于具有多个属性;然而,如果你有一个包含两个int
s、一个string
、一个DateTime
和一个bool
的结构,你可以很快消耗大量内存。使用一个类,多个调用者可以共享对同一个实例的引用(引用很小)。
【讨论】:
嗯,是的,但编译器就是这么愚蠢。恕我直言,不允许分配给 property-struct 成员是一个愚蠢的设计决定,因为它 is 允许++
运算符。在这种情况下,编译器只是自己编写显式赋值而不是催促程序员。
@Konrad: myObj.SomeProperty.Size = 22 将修改 myObj.SomeProperty 的副本。编译器使您免于明显的错误。而且 ++ 是不允许的。
@Konrad - 少了一个间接,它应该可以工作;它是“改变仅作为堆栈上的瞬态值存在并且即将蒸发为虚无的东西的值”,这就是被阻止的情况。
@Marc Gravell:在前一段代码中,您最终得到一个名为“abc”且其他属性为 List[0] 的“Foo”,而不会干扰 List[0 ]。如果 Foo 是一个类,则有必要克隆它然后更改副本。在我看来,值类型与类区别的最大问题是“。”的使用。运算符有两个目的。如果我有我的 druthers,类可以同时支持“。”和“->”表示方法和属性,但“。”的正常语义。 properties 将创建一个修改了适当字段的新实例。
@Backwards_Dave 那么您可能正在比较不同的场景;要么SomeProperty
实际上不是一个属性(也许它是一个字段?),要么SomeProperty
的type 实际上不是struct
。这是显示 CS1612 的最小复制:sharplab.io/…【参考方案3】:
我不会说邪恶,但可变性通常是程序员过度渴望提供最大功能的标志。实际上,这通常是不需要的,而这反过来又使界面更小、更容易使用和更难用错(= 更健壮)。
其中一个例子是竞态条件下的读/写和写/写冲突。这些根本不可能发生在不可变结构中,因为写入不是有效的操作。
Also, I claim that mutability is almost never actually needed,程序员只是认为它可能在未来。例如,更改日期根本没有意义。相反,根据旧日期创建一个新日期。这是一个廉价的操作,所以性能不是考虑因素。
【讨论】:
Eric Lippert 说他们是……看我的回答。 尽管我很尊重 Eric Lippert,但他不是上帝(或者至少现在还不是)。您链接到的博客文章和您上面的文章当然是使结构不可变的合理论据,但作为 never 使用可变结构的论据,它们实际上非常弱。但是,此帖子是 +1。 在 C# 中进行开发时,您通常不时需要可变性 - 特别是在您的业务模型中,您希望流式处理等与现有解决方案顺利工作。我写了一篇关于如何处理可变和不可变数据的文章,解决了关于可变性的大多数问题(我希望):rickyhelgesson.wordpress.com/2012/07/17/… @StephenMartin:封装单个值的结构通常应该是不可变的,但结构是迄今为止封装固定的独立但相关变量集(如点的 X 和 Y 坐标)的最佳媒介作为一个群体没有“身份”。用于那个目的的结构通常应该将它们的变量公开为公共字段。我认为使用类比结构更适合用于此类目的的概念是完全错误的。不可变类通常效率较低,而可变类通常具有可怕的语义。 @StephenMartin:例如,考虑一个应该返回图形转换的六个float
组件的方法或属性。如果这样的方法返回一个包含六个组件的暴露字段结构,很明显修改该结构的字段不会修改从中接收它的图形对象。如果这样的方法返回一个可变的类对象,也许改变它的属性会改变底层的图形对象,也许它不会——没人知道。【参考方案4】:
可变结构并不邪恶。
它们在高性能环境中是绝对必要的。例如,当缓存行和/或垃圾收集成为瓶颈时。
我不会将在这些完全有效的用例中使用不可变结构称为“邪恶”。
我可以看出,C# 的语法无助于区分值类型或引用类型的成员的访问,所以我完全赞成更喜欢不可变结构,它强制执行不变性, 在可变结构上。
但是,我建议不要简单地将不可变结构标记为“邪恶”,而是采用这种语言并提倡更有帮助和建设性的经验法则。
例如:“结构体是值类型,默认复制,不想复制需要引用”或 “首先尝试使用只读结构”。
【讨论】:
我还认为,如果想用胶带将一组固定的变量固定在一起,以便可以单独或作为一个单元处理或存储它们的值,那么问更有意义编译器将一组固定的变量固定在一起(即声明一个带有公共字段的struct
),而不是定义一个可以笨拙地用于实现相同目的的类,或者将一堆垃圾添加到结构中以让它模拟这样一个类(而不是让它表现得像一组用胶带粘在一起的变量,这是人们首先真正想要的)【参考方案5】:
具有公共可变字段或属性的结构并不邪恶。
改变“this”的结构方法(与属性设置器不同)有些邪恶,只是因为.net 没有提供将它们与没有的方法区分开来的方法。不改变“this”的结构方法即使在只读结构上也应该是可调用的,不需要防御性复制。改变“this”的方法根本不应该在只读结构上调用。由于 .net 不想禁止不修改“this”的结构方法在只读结构上被调用,但又不想允许只读结构发生突变,它防御性地复制只读结构中的结构。只有上下文,可以说是两全其美。
尽管在只读上下文中处理自变异方法存在问题,但可变结构通常提供远优于可变类类型的语义。考虑以下三个方法签名:
结构 PointyStruct public int x,y,z;; 类 PointyClass public int x,y,z;; 无效方法1(PointyStruct foo); void Method2(ref PointyStruct foo); void Method3(PointyClass foo);针对每种方法,回答以下问题:
-
假设该方法不使用任何“不安全”代码,它可能会修改 foo 吗?
如果在调用该方法之前不存在对“foo”的外部引用,那么之后是否存在外部引用?
答案:
问题 1:
Method1()
:没有(明确的意图)Method2()
:是(明确意图)Method3()
:是的(不确定的意图) 问题2:Method1()
:没有Method2()
:没有(除非不安全)Method3()
:是的
Method1 不能修改 foo,并且永远不会得到引用。 Method2 获取对 foo 的短期引用,它可以使用它以任何顺序修改 foo 的字段任意次数,直到它返回,但它不能持久化该引用。在 Method2 返回之前,除非它使用了不安全的代码,否则任何和所有可能由其 'foo' 引用制作的副本都将消失。 Method3 与 Method2 不同,它获得对 foo 的混杂可共享的引用,并且不知道它可以用它做什么。它可能根本不会改变 foo,它可能会改变 foo 然后返回,或者它可能会将 foo 的引用提供给另一个线程,这可能会在未来的某个任意时间以某种任意方式对其进行变异。限制 Method3 对传入其中的可变类对象可能执行的操作的唯一方法是将可变对象封装到只读包装器中,这既丑陋又麻烦。
结构数组提供了美妙的语义。给定 Rectangle 类型的 RectArray[500],很清楚如何例如将元素 123 复制到元素 456,然后稍后将元素 123 的宽度设置为 555,而不会干扰元素 456。“RectArray[432] = RectArray[321]; ...; RectArray[123].Width = 555;” .知道 Rectangle 是一个结构体,其中包含一个名为 Width 的整数字段,这将告诉人们所有需要了解的上述语句。
现在假设 RectClass 是一个与 Rectangle 具有相同字段的类,并且想要对 RectClass 类型的 RectClassArray[500] 执行相同的操作。也许该数组应该包含 500 个对可变 RectClass 对象的预初始化不可变引用。在这种情况下,正确的代码将类似于“RectClassArray[321].SetBounds(RectClassArray[456]); ...; RectClassArray[321].X = 555;”。也许该数组被假定包含不会更改的实例,因此正确的代码更像是“RectClassArray[321] = RectClassArray[456]; ...; RectClassArray[321] = New RectClass(RectClassArray[321] ]); RectClassArray[321].X = 555;"要知道一个人应该做什么,就必须了解更多关于 RectClass(例如,它是否支持复制构造函数、复制方法等)和数组的预期用途。远没有使用结构那么干净。
可以肯定的是,不幸的是,除了数组之外的任何容器类都没有很好的方法来提供结构数组的清晰语义。如果一个人想要一个集合被索引,那么最好的一个可以做。一个字符串,可能会提供一个通用的“ActOnItem”方法,该方法将接受一个字符串作为索引、一个通用参数和一个委托,该委托将通过引用通用参数和集合项来传递。这将允许与结构数组几乎相同的语义,但除非 vb.net 和 C# 人员能够提供良好的语法,否则即使性能合理(传递泛型参数会允许使用静态委托,并避免创建任何临时类实例)。
就个人而言,我对 Eric Lippert 等人的仇恨感到恼火。 spew 关于可变值类型。与到处使用的混杂引用类型相比,它们提供了更清晰的语义。尽管 .net 对值类型的支持存在一些限制,但在许多情况下,可变值类型比任何其他类型的实体更适合。
【讨论】:
@Ron Warholic:SomeRect 是一个矩形并不明显。它可能是一些其他类型,可以从 Rectangle 隐式类型转换。虽然,唯一可以从 Rectangle 隐式类型转换的系统定义类型是 RectangleF,如果尝试将 RectangleF 的字段传递给 Rectangle 的构造函数(因为前者是 Single,而后者是 Integer),编译器会尖叫,可能存在允许此类隐式类型转换的用户定义结构。顺便说一句,无论 SomeRect 是 Rectangle 还是 RectangleF,第一个语句都同样适用。 您所展示的只是在一个人为的示例中,您认为一种方法更清晰。如果我们以Rectangle
为例,我可以很容易地想出一个常见的情况,你会得到高度unclear 的行为。考虑 WinForms 实现了一个可变的 Rectangle
类型,用于表单的 Bounds
属性。如果我想改变界限,我想使用你的好语法:form.Bounds.X = 10;
但是这改变了表单上的 nothing (并生成一个可爱的错误通知你)。不一致是编程的祸根,也是需要不变性的原因。
@Ron Warholic:顺便说一句,我希望能够说“form.Bounds.X = 10;”让它正常工作,但系统没有提供任何干净的方法。将值类型属性公开为接受回调的方法的约定可以提供比任何使用类的方法更清洁、高效和可确认正确的代码。
这个答案比一些投票率最高的答案更有洞察力。反对可变值类型的论点依赖于当你混合别名和突变时“你期望什么”的概念,这有点荒谬。这是一件可怕的事情无论如何!
@supercat:谁知道呢,也许他们正在谈论的 C# 7 的 ref-return 功能可能涵盖了这个基础(我实际上并没有详细查看它,但表面上听起来很相似) .【参考方案6】:
从程序员的角度来看,还有一些其他极端情况可能会导致不可预知的行为。
不可变值类型和只读字段
// Simple mutable structure.
// Method IncrementI mutates current state.
struct Mutable
public Mutable(int i) : this()
I = i;
public void IncrementI() I++;
public int I get; private set;
// Simple class that contains Mutable structure
// as readonly field
class SomeClass
public readonly Mutable mutable = new Mutable(5);
// Simple class that contains Mutable structure
// as ordinary (non-readonly) field
class AnotherClass
public Mutable mutable = new Mutable(5);
class Program
void Main()
// Case 1. Mutable readonly field
var someClass = new SomeClass();
someClass.mutable.IncrementI();
// still 5, not 6, because SomeClass.mutable field is readonly
// and compiler creates temporary copy every time when you trying to
// access this field
Console.WriteLine(someClass.mutable.I);
// Case 2. Mutable ordinary field
var anotherClass = new AnotherClass();
anotherClass.mutable.IncrementI();
// Prints 6, because AnotherClass.mutable field is not readonly
Console.WriteLine(anotherClass.mutable.I);
可变值类型和数组
假设我们有一个Mutable
结构的数组,并且我们为该数组的第一个元素调用IncrementI
方法。您期望从这个电话中获得什么行为?它应该改变数组的值还是只改变一个副本?
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 可以通过多种方式安全有效地完成此类事情(例如,通过定义一个特殊的“ArrayRefT[]
和一个整数索引,并提供对@987654326 类型属性的访问权限@ 将被解释为对适当数组元素的访问)[如果一个类想要公开一个 ArrayRef<T>
用于任何其他目的,它可以提供一个方法——而不是一个属性——来检索它]。不幸的是,没有这样的规定。值类型基本上代表了不可变的概念。 Fx,拥有一个整数、向量等数学值然后能够对其进行修改是没有意义的。这就像重新定义一个值的含义。与其更改值类型,不如分配另一个唯一值更有意义。考虑一下通过比较其属性的所有值来比较值类型的事实。关键是,如果属性相同,那么它就是该值的相同通用表示。
正如 Konrad 提到的那样,更改日期也没有意义,因为该值表示唯一的时间点,而不是具有任何状态或上下文相关性的时间对象的实例。
希望这对您有意义。可以肯定的是,它更多的是关于你试图用值类型捕捉的概念,而不是实际的细节。
【讨论】:
嗯,它们应该代表不可变的概念,至少 ;-p 好吧,我想他们可以让 System.Drawing.Point 不可变,但恕我直言,这将是一个严重的设计错误。我认为积分实际上是一种典型的价值类型,它们是可变的。除了真正的早期编程 101 初学者之外,它们不会对任何人造成任何问题。 原则上我认为点也应该是不可变的,但如果它使类型更难使用或不那么优雅,那么当然也必须考虑这一点。如果没有人愿意使用它们,那么拥有维护最好原则的代码结构是没有意义的;) 值类型对于表示简单的不可变概念很有用,但暴露字段结构是用于保存或传递小的固定相关但独立值集的最佳类型(例如观点)。这种值类型的存储位置封装了其字段的值,仅此而已。相比之下,可变引用类型的存储位置可用于保存可变对象的状态,但也封装了整个宇宙中存在于同一对象的所有其他引用的标识。 “值类型基本上代表了不可变的概念”。 不,它们没有。值类型变量最古老和最有用的应用之一是int
迭代器,如果它是不可变的,它将完全没用。我认为您将“值类型的编译器/运行时实现”与“类型化为值类型的变量”混为一谈——后者肯定对任何可能的值都是可变的。【参考方案8】:
如果您曾经使用 C/C++ 之类的语言进行过编程,那么结构体可以用作可变结构。只需将它们与 ref 一起传递,就不会出错。我发现的唯一问题是 C# 编译器的限制,在某些情况下,我无法强迫愚蠢的东西使用对结构的引用,而不是复制(比如当结构是 C# 类的一部分时)。
所以,可变结构并不是邪恶的,C# 已经使它们变得邪恶。我一直在 C++ 中使用可变结构,它们非常方便和直观。相比之下,C# 让我完全放弃将结构作为类的成员,因为它们处理对象的方式。他们的便利让我们付出了代价。
【讨论】:
拥有结构类型的类字段通常是一种非常有用的模式,尽管承认有一些限制。如果使用属性而不是字段,或者使用readonly
,性能将会下降,但如果避免做这些事情,结构类型的类字段就可以了。结构唯一真正基本的限制是,像int[]
这样的可变类类型的结构字段可以封装标识或一组不变的值,但不能用于封装可变值而不封装不需要的标识。【参考方案9】:
如果你坚持结构的用途(在 C#、Visual Basic 6、Pascal/Delphi、C++ 结构类型(或类)中,当它们不用作指针时),你会发现结构不超过复合变量。这意味着:您会将它们视为一组打包的变量,使用一个通用名称(您引用成员的记录变量)。
我知道这会让很多习惯于 OOP 的人感到困惑,但如果使用得当,这还不足以说明这些东西本质上是邪恶的。一些结构按照它们的意图是不可变的(这是 Python 的 namedtuple
的情况),但它是另一个需要考虑的范式。
是的:结构体涉及大量内存,但这样做不会恰好是更多内存:
point.x = point.x + 1
相比:
point = Point(point.x + 1, point.y)
在不可变情况下,内存消耗至少相同,甚至更多(尽管对于当前堆栈而言,这种情况是暂时的,具体取决于语言)。
但是,最后,结构是结构,而不是对象。在POO中,一个对象的主要属性是它们的identity,大多数时候不超过它的内存地址。 struct 代表数据结构(不是一个适当的对象,因此它们无论如何都没有身份),并且可以修改数据。在其他语言中,record(而不是 struct,就像 Pascal 的情况一样)是一个词并且具有相同的目的:只是一个数据记录变量,旨在被读取从文件、修改和转储到文件中(这是主要用途,在许多语言中,您甚至可以在记录中定义数据对齐,而正确调用的对象则不一定如此)。
想要一个好例子吗?结构用于轻松读取文件。 Python 有this library,因为它是面向对象的并且不支持结构,它必须以另一种方式实现它,这有点难看。实现结构的语言具有该功能......内置。尝试使用 Pascal 或 C 等语言中的适当结构读取位图标头。这将很容易(如果结构正确构建和对齐;在 Pascal 中,您不会使用基于记录的访问,而是使用函数来读取任意二进制数据)。因此,对于文件和直接(本地)内存访问,结构比对象更好。至于今天,我们已经习惯了 JSON 和 XML,因此我们忘记了二进制文件的使用(以及作为副作用,结构的使用)。但是是的:它们存在,并且有目的。
他们并不邪恶。只需将它们用于正确的目的。
如果你用锤子来思考,你会想把螺丝当钉子,发现螺丝更难插在墙上,那是螺丝的错,是坏的。
【讨论】:
【参考方案10】:假设您有一个包含 1,000,000 个结构的数组。每个结构都代表一个股权,包含bid_price、offer_price(可能是小数)等,这是由C#/VB创建的。
想象一下,该数组是在非托管堆中分配的一块内存中创建的,以便其他一些本机代码线程能够同时访问该数组(也许是一些执行数学运算的高性能代码)。
假设 C#/VB 代码正在监听价格变化的市场反馈,该代码可能必须访问数组的某些元素(无论是哪种证券),然后修改某些价格字段。
想象一下每秒执行数万甚至数十万次。
好吧,让我们面对事实吧,在这种情况下,我们确实希望这些结构是可变的,它们必须是可变的,因为它们被其他一些本机代码共享,因此创建副本无济于事;之所以需要这样做,是因为以这样的速率复制大约 120 字节的结构是愚蠢的,尤其是当更新实际上可能只影响一两个字节时。
雨果
【讨论】:
没错,但在这种情况下,使用结构的原因是这样做是由外部约束(由本机代码使用的约束)强加给应用程序设计的。您描述的关于这些对象的所有其他内容都表明它们显然应该是 C# 或 VB.NET 中的类。 我不确定为什么有些人认为这些东西应该是类对象。如果所有数组槽都填充了引用不同的实例,则使用类类型将额外增加 12 或 24 个字节的内存需求,并且对类对象引用数组的顺序访问往往比在结构数组。【参考方案11】:当某些东西可以变异时,它就会获得一种认同感。
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 的位置 比克隆Eric、移动克隆体并破坏原件 更自然。这两种操作都可以成功地改变eric.position
的内容,但一种比另一种更直观。同样,传递 Eric(作为参考)以获取修改他的方法更直观。给一个方法一个 Eric 的克隆几乎总是令人惊讶。任何想要修改 Person
的人都必须记得请求对 Person
的引用,否则他们会做错事。
如果你让类型不可变,问题就消失了;如果我无法修改eric
,我收到eric
或eric
的克隆对我没有任何影响。更一般地说,如果一个类型的所有可观察状态都保存在以下成员中,则该类型可以安全地按值传递:
如果满足这些条件,那么可变值类型的行为类似于引用类型,因为浅拷贝仍然允许接收者修改原始数据。
不可变Person
的直观性取决于您尝试执行的操作。如果Person
只是代表一个人的数据集,那么它并没有什么不直观的地方; Person
变量真正代表抽象值,而不是对象。 (在这种情况下,将其重命名为 PersonData
可能更合适。)如果 Person
实际上是在为一个人本身建模,那么即使您已经避免了陷阱,不断创建和移动克隆的想法也是愚蠢的认为你正在修改原件。在这种情况下,简单地将Person
设为引用类型(即类)可能会更自然。
当然,正如函数式编程告诉我们的那样,使 一切 不可变是有好处的(没有人可以偷偷持有对 eric
的引用并改变他),但因为这在OOP 对于使用您的代码的其他任何人来说,它仍然是不直观的。
【讨论】:
你关于身份的观点很好;可能值得注意的是,只有当对某事物存在多个引用时,身份才是相关的。如果foo
在宇宙中的任何地方拥有对其目标的唯一引用,并且没有任何东西捕获该对象的身份哈希值,那么变异字段foo.X
在语义上等同于使foo
指向一个新对象,就像它之前提到的那个,但X
持有所需的值。对于类类型,通常很难知道是否存在对某事物的多个引用,但对于结构则很容易:它们不存在。
如果Thing
是一个可变类类型,那么Thing[]
将封装对象身份——无论你是否愿意——除非可以确保没有存在任何外部引用的数组中的Thing
将永远被突变。如果不希望数组元素封装身份,通常必须确保它所持有的任何项目都不会发生变异,或者它所持有的任何项目都不会存在外部引用[混合方法也可以工作]。这两种方法都不是非常方便。如果Thing
是一个结构,则Thing[]
只封装值。
对于对象,它们的身份来自于它们的位置。引用类型的实例由于它们在内存中的位置而具有它们的标识,并且您只传递它们的标识(引用),而不是它们的数据,而值类型在它们存储的外部位置具有它们的标识。您的 Eric 值类型的标识仅来自存储他的变量。如果你让他四处走动,他就会失去身份。【参考方案12】:
它与结构无关(也与 C# 无关),但在 Java 中,当可变对象是例如可变对象时,您可能会遇到问题。哈希映射中的键。如果您在将它们添加到地图后更改它们并更改其hash code,则可能会发生邪恶的事情。
【讨论】:
如果您也使用类作为映射中的键,情况也是如此。【参考方案13】:可变数据有很多优点和缺点。百万美元的劣势是混叠。如果在多个地方使用相同的值,并且其中一个更改了它,那么它似乎已经神奇地更改为正在使用它的其他地方。这与竞态条件有关,但并不完全相同。
百万美元的优势有时是模块化。可变状态可以让您从不需要知道的代码中隐藏变化的信息。
The Art of the Interpreter 详细介绍了这些权衡,并给出了一些示例。
【讨论】:
结构在 c# 中没有别名。每个结构赋值都是一个副本。 @recursive:在某些情况下,这是可变结构的主要优势,这让我质疑结构不应该是可变的概念。编译器有时会隐式复制结构这一事实并不会降低可变结构的实用性。【参考方案14】:就我个人而言,当我查看代码时,以下代码对我来说看起来很笨拙:
data.value.set(data.value.get()+1);
而不是简单
data.value++ ;或 data.value = data.value + 1 ;
数据封装在传递类时很有用,并且您希望确保以受控方式修改值。但是,当您有公共的 set 和 get 函数只是将值设置为传入的值时,与简单地传递公共数据结构相比,这有什么改进?
当我在类中创建私有结构时,我创建了该结构以将一组变量组织到一个组中。我希望能够在类范围内修改该结构,而不是获取该结构的副本并创建新实例。
对我来说,这会阻止有效使用用于组织公共变量的结构,如果我想要访问控制,我会使用一个类。
【讨论】:
直奔主题!结构是没有访问控制限制的组织单元!不幸的是,C# 使它们无法用于此目的! 这 完全 没有抓住重点,因为您的两个示例都显示了可变结构。 C# 使它们无法用于此目的,因为这不是结构的目的【参考方案15】:Eric Lippert 先生的例子有几个问题。它旨在说明复制结构的意义以及如果您不小心,这可能会成为问题。看这个例子,我认为它是由于不良的编程习惯造成的,而不是结构或类的问题。
结构应该只有公共成员,不需要任何封装。如果是这样,那么它真的应该是一个类型/类。你真的不需要两个结构来说同样的事情。
如果你有一个包含结构的类,你会调用类中的一个方法来改变成员结构。这就是我会做的一个好的编程习惯。
正确的实现如下。
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());
看起来这是编程习惯的问题,而不是结构本身的问题。结构应该是可变的,这就是想法和意图。
变化的结果如预期的那样:
1 2 3 按任意键继续 。 . .
【讨论】:
将小的不透明结构设计成表现得像不可变的类对象并没有错; MSDN 指南是合理的当一个人试图制作行为像一个对象的东西时。在某些情况下,结构是合适的,其中需要像对象一样的轻量级事物,以及需要将一堆变量用胶带粘在一起的情况。然而,出于某种原因,许多人没有意识到结构有两种不同的用法,并且适用于一种的准则不适用于另一种。【参考方案16】:如果使用得当,我不相信它们是邪恶的。我不会将它放在我的生产代码中,但我会用于结构化单元测试模拟之类的东西,其中结构的生命周期相对较短。
使用 Eric 示例,也许您想创建该 Eric 的第二个实例,但要进行调整,因为这是您的测试的性质(即复制,然后修改)。如果我们只是将 Eric2 用于测试脚本的其余部分,那么 Eric 的第一个实例会发生什么并不重要,除非您打算将他用作测试比较。
这对于测试或修改浅层定义特定对象(结构点)的遗留代码非常有用,但是通过具有不可变结构,这可以防止它令人讨厌地使用。
【讨论】:
在我看来,结构的核心是一堆用胶带粘在一起的变量。在 .NET 中,结构可以假装不是用胶带粘在一起的一堆变量,我建议在实用时,一种类型会假装是一堆粘在一起的变量以外的东西使用胶带应该表现为一个统一的对象(这对于结构意味着不变性),但有时将一堆变量与胶带粘在一起很有用。即使在生产代码中,我也会认为最好有一个类型...... ...除了“每个字段都包含最后写入的内容”之外,显然没有语义,将所有语义推入使用结构的代码中,而不是尝试让结构做更多事情。例如,给定一个Range<T>
类型,其中成员Minimum
和Maximum
类型为T
字段以及代码Range<double> myRange = foo.getRange();
,任何关于Minimum
和Maximum
包含内容的保证都应该来自foo.GetRange();
.让 Range
成为暴露字段结构将清楚地表明它不会添加自己的任何行为。以上是关于为啥可变结构是“邪恶的”?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 C# 数组对 Enumeration 使用引用类型,而 List<T> 使用可变结构?
为啥 &mut self 允许借用 struct 成员,但不允许将 self 借用到不可变方法?