为什么可变结构是“邪恶的”?

Why are mutable structs “evil”?

提问人:Dirk Vollmar 提问时间:1/14/2009 最后编辑:CommunityDirk Vollmar 更新时间:5/2/2020 访问量:104956

问:

在 SO 上的讨论之后,我已经多次读到可变结构是“邪恶的”评论(就像这个问题的答案一样)。

C#中的可变性和结构的实际问题是什么?

C# 结构 不可变性 可变

评论

24赞 Slipp D. Thompson 7/21/2014
声称可变结构是邪恶的,就像声称可变的 s、s 和所有其他值类型都是邪恶的一样。有可变性和不变性的情况。这些情况取决于数据所扮演的角色,而不是内存分配/共享的类型。intbool
63赞 Blorgbeard 3/5/2015
@slipp并且可变。.intbool
2赞 Slipp D. Thompson 5/6/2015
... -语法,使 ref 类型化数据和值类型化数据的操作看起来相同,即使它们明显不同。这是 C# 属性的错误,而不是结构的错误——某些语言提供了用于就地变异的替代语法。在 C# 中,最好提供结构成员赋值器方法,例如“MutateV(Action<ref Vector2> mutator)”并像这样使用它(由于 C# 对 ref 关键字的限制,示例过于简化,但应该可以使用一些解决方法)。.a[V][X] = 3.14a.MutateV((v) => { v.X = 3; })
3赞 Sushi271 5/6/2015
@Slipp嗯,我对这类结构的看法完全相反。为什么您认为已经在 .NET 库中实现的结构,如 DateTime 或 TimeSpan(如此相似的结构)是不可变的?也许只更改这种结构的 var 的一个成员可能很有用,但它太不方便了,导致太多问题。实际上,您对处理器计算的内容是错误的,因为 C# 不会编译为汇编程序,而是编译为 IL。在 IL 中(前提是我们已经有名为 的变量),这个单一操作是 4 条指令:(将 0-index 变量加载到...xldloc.0
1赞 Sushi271 5/6/2015
...类型。 是类型。Ref 只是一个关键字,它使变量传递给方法本身,而不是它的副本。它对引用类型也有意义,因为我们可以更改变量,即方法外部的引用在方法内更改后将指向其他对象。由于不是类型,而是传递方法参数的方式,因此不能将其放入,因为只能将类型放在那里。所以这是不正确的。也许这样做会很方便,也许 C# 团队可以为一些新版本制作这个,但现在他们正在开发一些......Tref T<>

答:

181赞 Marc Gravell 1/14/2009 #1

从哪里开始;-p

埃里克·利珀特(Eric Lippert)的博客总是适合引用:

这是可变的另一个原因 值类型是邪恶的。尝试始终 使值类型不可变。

首先,你往往很容易失去变化......例如,从列表中获取内容:

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;

不太关键的是,存在尺寸问题;可变对象往往具有多个属性;然而,如果你有一个包含两个 s、a、a 和 a 的结构,你可以非常快速地烧毁大量内存。对于一个类,多个调用方可以共享对同一实例的引用(引用很小)。intstringDateTimebool

评论

6赞 Konrad Rudolph 1/14/2009
嗯,是的,但编译器就是这样愚蠢的。恕我直言,不允许分配给属性结构成员是一个愚蠢的设计决策,因为它允许运算符的。在这种情况下,编译器只是编写显式赋值本身,而不是让程序员忙碌。++
14赞 Lucas 5/15/2009
@Konrad:myObj.SomeProperty.Size = 22 将修改 myObj.SomeProperty 的 COPY。编译器将您从明显的错误中拯救出来。并且不允许++。
7赞 Marc Gravell 9/20/2010
@Konrad - 少一个间接性,它应该可以工作;它是“改变仅作为瞬态值存在于堆栈上并即将蒸发成虚无的事物的值”,这是被阻止的情况。
2赞 supercat 10/10/2010
@Marc Gravell:在前一段代码中,你最终会得到一个“Foo”,它的名字是“abc”,其其他属性是 List[0],而不会干扰 List[0]。如果 Foo 是一个类,则需要克隆它,然后更改副本。在我看来,值类型与类区别的最大问题是将“.”运算符用于两个目的。如果我有我的 druther,类可以同时支持方法和属性的“.”和“->”,但“.”属性的正常语义是创建一个修改了相应字段的新实例。
1赞 Marc Gravell 4/3/2018
那么,@Backwards_Dave您可能正在比较不同的场景;要么实际上不是一个属性(也许它是一个字段?),要么 的类型实际上不是一个 .这是显示 CS1612 的最小重现:sharplab.io/...SomePropertySomePropertystruct
6赞 Bombe 1/14/2009 #2

它与结构没有任何关系(也与 C# 无关),但在 Java 中,当可变对象是哈希映射中的键时,您可能会遇到问题。如果您在将它们添加到地图后更改它们并且它更改了其哈希代码,则可能会发生邪恶的事情。

评论

3赞 Marc Gravell 1/14/2009
如果在映射中使用类作为键,则也是如此。
80赞 Konrad Rudolph 1/14/2009 #3

我不会说邪恶,但可变性通常是程序员过度渴望提供最大功能的标志。实际上,这通常不是必需的,这反过来又使界面更小,更易于使用,更难使用错误(=更健壮)。

例如,竞争条件下的读/写和写/写冲突。这些根本不可能发生在不可变的结构中,因为写入不是有效的操作。

另外,我声称几乎实际上从来不需要可变性程序员只是认为它可能在未来。例如,更改日期根本没有意义。相反,基于旧日期创建一个新日期。这是一种廉价的操作,因此性能不是考虑因素。

评论

1赞 Marc Gravell 1/14/2009
埃里克·利珀特(Eric Lippert)说他们是......看看我的答案。
50赞 Stephen Martin 1/14/2009
尽管我尊重埃里克·利珀特(Eric Lippert),但他不是上帝(或者至少现在还不是)。你链接到的博客文章和你上面的帖子是使结构不可变的合理论据,但实际上它们作为永远不使用可变结构的论据非常薄弱。然而,这篇文章是+1。
2赞 Ricky Helgesson 7/31/2012
在 C# 中进行开发时,您通常需要时不时地进行可变性 - 尤其是在您的业务模型中,您希望流式处理等能够与现有解决方案顺利配合。我写了一篇关于如何使用可变和不可变数据的文章,解决了围绕可变性的大多数问题(我希望):rickyhelgesson.wordpress.com/2012/07/17/......
4赞 supercat 6/5/2013
@StephenMartin:封装单个值的结构通常应该是不可变的,但结构是迄今为止封装固定的独立但相关的变量集(如点的 X 和 Y 坐标)的最佳媒介,这些变量没有“身份”作为一个组。用于目的的结构通常应将其变量公开为公共字段。我认为使用类比结构更适合用于此类目的的想法是完全错误的。不可变类通常效率较低,可变类通常具有可怕的语义。
3赞 supercat 6/5/2013
@StephenMartin:例如,考虑一个方法或属性,该方法或属性应该返回图形转换的六个组件。如果这样的方法返回一个包含六个组件的 exposed-field 结构,很明显,修改结构的字段不会修改从中接收它的图形对象。如果这样的方法返回一个可变的类对象,也许更改其属性会更改底层图形对象,也许它不会 - 没有人真正知道。float
326赞 trampster 1/14/2009 #4

结构是值类型,这意味着它们在传递时会被复制。

因此,如果您更改副本,则只会更改该副本,而不是原始副本,也不会更改可能存在的任何其他副本。

如果你的结构是不可变的,那么所有按值传递的自动副本都将是相同的。

如果你想改变它,你必须有意识地通过使用修改后的数据创建一个结构的新实例来实现。(不是副本)

评论

95赞 Lucas 5/15/2009
“如果你的结构是不可变的,那么所有的副本都是相同的。”不,这意味着如果你想要一个不同的值,你必须有意识地复制一个。这意味着您不会被发现认为您正在修改原始副本。
27赞 trampster 5/27/2010
@Lucas 我认为你说的是另一种副本,我说的是按价值传递而制作的自动副本,你的“有意识的副本”是故意的,你不是错误地制作的,它不是真正的副本,它是一个包含不同数据的故意的新时刻。
6赞 Lucas 5/27/2010
您的编辑(16 个月后)使这一点更加清晰。不过,我仍然坚持“(不可变结构)意味着你不会被发现认为你正在修改原始副本”。
7赞 supercat 10/29/2011
@Lucas:复制一个结构体,修改它,并以某种方式认为一个人正在修改原始结构体(当一个人正在编写一个结构体字段的事实使一个人只是在编写自己的副本这一事实变得不言而喻)的危险似乎很小,与持有类对象作为保存其中包含的信息的手段的人将改变对象以更新其自己的信息并在该进程损坏了其他对象保存的信息。
6赞 Saeb Amini 5/22/2016
第 3 段听起来是错误的或不清楚的。如果你的结构是不可变的,那么你根本无法修改它的字段或任何副本的字段。“如果你想改变它,你必须......”这也是误导性的,你永远无法改变,无论是有意识的还是无意识的。创建一个新实例,您想要的数据除了具有相同的数据结构外,与原始副本无关。
6赞 Andru Luvisi 1/14/2009 #5

可变数据有许多优点和缺点。百万美元的缺点是混叠。如果同一个值在多个地方被使用,并且其中一个地方改变了它,那么它似乎已经神奇地改变了使用它的其他地方。这与竞争条件相关,但并不完全相同。

有时,百万美元的优势是模块化。可变状态可以让你从不需要知道的代码中隐藏不断变化的信息。

《诠释者的艺术》详细地探讨了这些权衡,并举了一些例子。

评论

0赞 recursive 9/25/2010
结构在 C# 中不具有别名。每个结构赋值都是一个副本。
0赞 supercat 10/10/2010
@recursive:在某些情况下,这是可变结构的一个主要优点,这让我质疑结构不应该是可变的观念。编译器有时会隐式复制结构,这一事实并没有降低可变结构的实用性。
23赞 Morten Christiansen 1/14/2009 #6

值类型基本上表示不可变的概念。Fx,拥有整数、向量等数学值然后能够修改它是没有意义的。这就像重新定义一个值的含义。与其更改值类型,不如分配另一个唯一值。考虑通过比较其属性的所有值来比较值类型的事实。关键是,如果属性相同,则它是该值的相同通用表示形式。

正如 Konrad 所提到的,更改日期也没有意义,因为该值表示该唯一时间点,而不是具有任何状态或上下文依赖关系的时间对象的实例。

希望这对您有意义。可以肯定的是,它更多的是关于您尝试使用值类型捕获的概念,而不是实际细节。

评论

4赞 Stephen Martin 1/14/2009
好吧,我想他们本可以使 System.Drawing.Point 不可变,但恕我直言,这将是一个严重的设计错误。我认为点实际上是一种典型的值类型,它们是可变的。除了真正早期的编程 101 初学者之外,它们不会给任何人带来任何问题。
4赞 Morten Christiansen 1/14/2009
原则上,我认为点也应该是不可变的,但如果它使类型更难使用或更不优雅,那么当然也必须考虑这一点。如果没有人愿意使用它们,那么拥有支持最佳原则的代码结构是没有意义的;)
4赞 supercat 6/14/2013
值类型对于表示简单的不可变概念很有用,但公开字段结构是用于保存或传递相关但独立的小固定值集(例如点的坐标)的最佳类型。此类值类型的存储位置封装其字段的值,而不封装其他任何值。相比之下,可变引用类型的存储位置可用于保存可变对象的状态,但也封装了整个宇宙中存在于同一对象的所有其他引用的标识。
6赞 Slipp D. Thompson 4/7/2015
“值类型基本上代表了不可变的概念”。不,他们没有。值类型变量最古老、最有用的应用之一是迭代器,如果它是不可变的,它将完全无用。我认为您将“值类型的编译器/运行时实现”与“类型化为值类型的变量”混为一谈——后者肯定可以可变为任何可能的值。int
1赞 Slipp D. Thompson 4/7/2015
按照你在这个答案中陈述的逻辑,所有类型都是不可变的。类存储为值类型和引用(内存地址指针/句柄)的集合——因此它们也是不可变的,因为您不更改内存地址,只需“分配另一个唯一值”。Q 显然是关于结构类别数据结构的拟议用法,从高级程序员的角度来看,在初始化后一次更改它们所包含的值和内存位置。将讨论切换到编译器优化使这 A 变得无关紧要。
14赞 Hugo 3/22/2010 #7

假设您有一个包含 1,000,000 个结构的数组。每个结构体都代表一个权益,其中包含 bid_price、offer_price(可能是小数)等内容,这是由 C#/VB 创建的。

想象一下,数组是在非托管堆中分配的内存块中创建的,以便其他一些本机代码线程能够同时访问该数组(可能是一些执行数学运算的高性能代码)。

想象一下,C#/VB 代码正在侦听价格变化的市场馈送,该代码可能必须访问数组的某些元素(无论哪种证券),然后修改某些价格字段。

想象一下,这是每秒数万甚至数十万次。

好吧,让我们面对事实,在这种情况下,我们确实希望这些结构是可变的,它们需要是可变的,因为它们被其他一些本机代码共享,所以创建副本无济于事;它们之所以需要,是因为以这样的速率复制大约 120 字节的结构是疯狂的,尤其是当更新实际上可能只影响一两个字节时。

雨 果

评论

3赞 Jon Hanna 10/15/2010
没错,但在这种情况下,使用结构的原因是这样做是通过外部约束(由本机代码的使用)强加给应用程序设计的。您描述的有关这些对象的所有其他内容都表明它们显然应该是 C# 或 VB.NET 中的类。
7赞 supercat 6/5/2013
我不确定为什么有些人认为这些东西应该是类对象。如果所有数组插槽都填充了不同的引用实例,则使用类类型将为内存需求增加额外的 12 或 24 个字节,并且对类对象引用数组的顺序访问往往比对结构数组的顺序访问慢得多。
6赞 Mike 2/8/2011 #8

就我个人而言,当我查看代码时,以下内容对我来说看起来很笨拙:

data.value.set ( data.value.get () + 1 ) ;

而不是简单地

数据值++ ;或 data.value = data.value + 1 ;

在传递类时,数据封装非常有用,并且您希望确保以受控方式修改值。但是,当您拥有公共集合并获取函数时,这些函数的作用只不过是将值设置为传入的内容,这如何比简单地传递公共数据结构有所改进?

当我在类中创建私有结构时,我创建了该结构以将一组变量组织到一个组中。我希望能够在类范围内修改该结构,而不是获取该结构的副本并创建新实例。

对我来说,这阻止了用于组织公共变量的结构的有效使用,如果我想要访问控制,我会使用一个类。

评论

1赞 ThunderGr 10/18/2013
开门见山!结构是没有访问控制限制的组织单元!不幸的是,C# 使它们无法用于此目的!
0赞 vidstige 9/29/2015
完全没有抓住重点,因为您的两个示例都显示了可变结构。
0赞 Luiz Felipe 2/10/2017
C# 使它们无法用于此目的,因为这不是结构的目的
27赞 Sergey Teplyakov 7/14/2011 #9

从程序员的角度来看,还有其他一些极端情况可能会导致不可预测的行为。

不可变值类型和只读字段

    // 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);
        }
    }

可变值类型和数组

假设我们有一个结构数组,并且我们正在调用该数组的第一个元素的方法。您期望从此调用中获得什么行为?它应该更改数组的值还是仅更改副本?MutableIncrementI

    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);

所以,只要你和团队的其他成员清楚地了解你在做什么,可变结构就不是邪恶的。但是,在很多极端情况下,程序行为与预期不同,这可能会导致微妙的难以产生和难以理解的错误。

评论

9赞 supercat 9/29/2011
如果 .net 语言对值类型的支持稍微好一点,那么应该发生的事情是,应该禁止结构方法改变“this”,除非它们被明确声明为这样做,并且应该禁止在只读上下文中如此声明的方法。可变结构数组提供了有用的语义,这是无法通过其他方式有效实现的。
2赞 Dave Cousineau 2/6/2012
这些都是可变结构可能产生的非常微妙的问题的好例子。我没想到会有这种行为。为什么数组会给你一个引用,而一个接口会给你一个值?我本来以为,除了一直值(这是我真正期望的)之外,它至少是相反的:接口提供引用;给出值的数组...
2赞 nawfal 10/8/2013
哦,我的天......这使得可变结构变得该死的邪恶!
1赞 springy76 10/19/2016
当你将突变方法重构为需要 ref 参数的静态方法时,编译器应该阻止你做“错误”的事情。public static void IncrementI(ref Mutable m) { m.I++; }
2赞 AnorZaken 8/31/2017
我喜欢这个答案,因为它包含非常有价值的信息,这些信息是不明显的。但实际上,这并不是像一些人声称的那样反对可变结构。是的,正如埃里克所说,我们在这里看到的是一个“绝望的深渊”,但这种绝望的根源不是可变性。绝望的根源是结构的自我变异方法。(至于为什么数组和列表的行为不同,这是因为一个基本上是计算内存地址的运算符,另一个是属性。一般来说,一旦你理解了“引用”是一个地址,一切都变得清晰了。
50赞 supercat 9/29/2011 #10

具有公共可变字段或属性的结构体并不邪恶。

改变“this”的结构方法(与属性设置器不同)有些邪恶,只是因为 .net 没有提供将它们与不改变的方法区分开来的方法。即使在只读结构上,不改变“this”的结构方法也应该是可调用的,而无需任何防御性复制。在只读结构上根本不应该调用改变“this”的方法。由于 .net 不想禁止在只读结构上调用不修改“this”的结构方法,但又不想允许只读结构发生突变,因此它会在只读上下文中防御性地复制结构,可以说是两全其美。

然而,尽管在只读上下文中处理自突变方法存在问题,但可变结构通常提供远远优于可变类类型的语义。请考虑以下三个方法签名:

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);

对于每种方法,请回答以下问题:

  1. 假设该方法不使用任何“不安全”代码,它可能会修改 foo 吗?
  2. 如果在调用方法之前不存在对“foo”的外部引用,那么调用方法之后是否存在外部引用?

答案:

问题 1: : 否 (明确意图) : 是 (明确意图) : 是 (不确定意图) 问题 2: : 否 : 否
(除非不安全)



: 是

Method1()Method2()Method3()Method1()Method2()Method3()

Method1 无法修改 foo,并且永远不会获得引用。Method2 获取对 foo 的短暂引用,它可以使用它以任何顺序修改 foo 的字段任意次数,直到它返回,但它不能保留该引用。在 Method2 返回之前,除非它使用不安全的代码,否则可能由其“foo”引用制作的任何和所有副本都将消失。与 Method2 不同,Method3 获得了对 foo 的可混杂共享引用,并且不知道它会用它做什么。它可能根本不会改变 foo,它可能会改变 foo 然后返回,或者它可能会将 foo 引用到另一个线程,该线程可能会在某个任意的未来时间以某种任意方式改变它。限制 Method3 对传递给它的可变类对象执行的操作的唯一方法是将可变对象封装到只读包装器中,这既丑陋又繁琐。

结构数组提供了美妙的语义。给定 Rectangle 类型的 RectArray[500],例如,如何将元素 123 复制到元素 456,然后在一段时间后将元素 123 的宽度设置为 555,而不干扰元素 456,这是很清楚的。“RectArray[432] = RectArray[321];...;矩形数组[123]。宽度 = 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] = 新建 RectClass(RectClassArray[321]);RectClassArray[321]。X = 555;”要知道应该做什么,就必须更多地了解 RectClass(例如,它是否支持复制构造函数、复制自方法等)和数组的预期用法。远不如使用结构体干净。

可以肯定的是,不幸的是,除了数组之外,任何容器类都没有很好的方法来提供结构数组的干净语义。如果想要用字符串等索引集合,最好的办法可能是提供一个通用的“ActOnItem”方法,该方法将接受索引的字符串、泛型参数和委托,该委托将通过引用传递泛型参数和集合项。这将允许与结构数组几乎相同的语义,但除非可以说服 vb.net 和 C# 人员提供一个很好的语法,否则即使代码具有合理的性能,代码看起来也会很笨拙(传递泛型参数将允许使用静态委托,并避免创建任何临时类实例的需要)。

就我个人而言,我对 Eric Lippert 等人对可变值类型的仇恨感到恼火。它们提供的语义比到处使用的混杂引用类型要干净得多。尽管 .net 对值类型的支持存在一些限制,但在许多情况下,可变值类型比任何其他类型的实体都更适合。

评论

1赞 supercat 10/28/2011
@Ron Warholic:SomeRect 是一个 Rectangle 并不是不言而喻的。它可能是其他一些类型,可以从 Rectangle 隐式类型转换。虽然,唯一可以从 Rectangle 隐式类型转换的系统定义类型是 RectangleF,如果试图将 RectangleF 的字段传递给 Rectangle 的构造函数(因为前者是 Single,后者是 Integer),编译器会发出嘶吼声,但可能存在允许用户定义的结构允许这种隐式类型转换。顺便说一句,无论 SomeRect 是 Rectangle 还是 RectangleF,第一条语句都同样有效。
0赞 Ron Warholic 10/29/2011
你所展示的只是,在一个人为的例子中,你认为一种方法更清晰。如果我们以你为例,我可以很容易地想出一个常见的坐姿,你会得到非常不明确的行为。假设 WinForms 实现了窗体属性中使用的可变类型。如果我想更改边界,我想使用您漂亮的语法:但是,这在表单上没有任何变化(并生成一个可爱的错误来通知您)。不一致是编程的祸根,也是需要不变性的原因。RectangleRectangleBoundsform.Bounds.X = 10;
3赞 supercat 10/29/2011
@Ron Warholic:顺便说一句,我希望能够说“形式”。Bounds.X = 10;“,让它正常工作,但系统没有提供任何干净的方式。与任何使用类的方法相比,将值类型属性公开为接受回调的方法的约定可以提供更干净、高效且可确认正确的代码。
5赞 Eamon Nerbonne 5/6/2016
这个答案比一些得票最高的答案更有见地。有点荒谬的是,反对可变值类型的论点依赖于“你期望”的概念,当你混合混淆和突变时会发生什么。无论如何,这是一件可怕的事情!
2赞 Eamon Nerbonne 5/6/2016
@supercat:谁知道呢,也许他们正在谈论的 C# 7 的 ref-return 功能可能涵盖了这个基础(我实际上还没有详细研究过它,但从表面上看它听起来很相似)。
66赞 JE42 6/4/2013 #11

可变结构不是邪恶的。

它们在高性能情况下是绝对必要的。例如,当缓存行和/或垃圾回收成为瓶颈时。

我不会称在这些完全有效的用例中使用不可变结构体是“邪恶的”。

我可以看到 C# 的语法无助于区分值类型或引用类型成员的访问,因此我完全赞成使用强制不可变性的不可变结构,而不是可变结构。

然而,与其简单地将不可变的结构贴上“邪恶”的标签,我建议接受这种语言并倡导更有用和建设性的经验法则。

例如:“structs 是值类型,默认情况下会复制。如果你不想复制它们,你需要一个引用“”先尝试使用只读结构”。

评论

11赞 supercat 6/5/2013
我还认为,如果想要将一组固定的变量与胶带固定在一起,以便它们的值可以单独处理或存储,也可以作为一个单元进行处理或存储,那么要求编译器将一组固定的变量固定在一起(即用公共字段声明一个)比定义一个可以使用的类更有意义, 笨拙的是,为了达到同样的目的,或者向结构中添加一堆垃圾,让它模拟这样的类(而不是让它表现得像一组用胶带粘在一起的变量,这是人们首先真正想要的)struct
10赞 Doval 10/2/2013 #12

当某物可以变异时,它就会获得一种认同感。

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));

因为是可变的,所以考虑改变埃里克的位置比克隆埃里克、移动克隆和破坏原件更自然。这两种操作都可以成功地更改 的内容,但一个操作比另一个操作更直观。同样,将 Eric 传递(作为参考)以获取修改他的方法也更直观。给一个方法一个Eric的克隆几乎总是令人惊讶的。任何想要变异的人都必须记得要求参考,否则他们会做错事。Personeric.positionPersonPerson

如果使类型不可变,问题就会消失;如果我不能修改,那么我收到还是克隆对我来说没有区别。更一般地说,如果某个类型的所有可观察状态都保存在以下成员中,则按值传递类型是安全的:ericericeric

  • 引用类型
  • 按值安全传递

如果满足这些条件,则可变值类型的行为类似于引用类型,因为浅拷贝仍允许接收方修改原始数据。

不过,不可变的直观性取决于你要做什么。如果只是代表一组关于一个人的数据,那么它没有什么不直观的; 变量真正表示抽象,而不是对象。(在这种情况下,将其重命名为 .如果真的在对一个人本身进行建模,那么不断创建和移动克隆的想法是愚蠢的,即使你已经避免了认为你正在修改原始人的陷阱。在这种情况下,简单地创建引用类型(即类)可能更自然。PersonPersonPersonPersonDataPersonPerson

诚然,正如函数式编程告诉我们的那样,使一切都不可变是有好处的(没有人可以偷偷地保留对他的引用并改变他),但由于这在 OOP 中不是惯用的,所以对于使用你的代码的其他人来说,它仍然会是不直观的。eric

评论

3赞 supercat 10/4/2013
你关于身份的观点很好;值得注意的是,只有当存在对某物的多个引用时,身份才有意义。如果在宇宙中任何地方都拥有对其目标的唯一引用,并且没有任何东西捕获该对象的身份哈希值,那么 mutating 字段在语义上等同于指向一个新对象,该对象与它之前引用的对象一样,但持有所需的值。对于类类型,通常很难知道是否存在对某物的多个引用,但对于结构,这很容易:它们不存在。foofoo.XfooX
1赞 supercat 10/4/2013
如果是一个可变的类类型,则 a 封装对象标识 - 无论是否愿意 - 除非可以确保存在任何外部引用的数组中不会发生任何突变。如果不希望数组元素封装标识,通常必须确保它所包含的引用的任何项都不会发生突变,或者它所包含的任何项都不会存在外部引用[混合方法也可以工作]。这两种方法都不是非常方便。如果是结构,则 a 仅封装值。ThingThing[]ThingThingThing[]
0赞 IS4 11/13/2014
对于对象,它们的身份来自它们的位置。由于引用类型的实例在内存中的位置,它们具有标识,并且您只传递它们的标识(引用),而不传递它们的数据,而值类型的标识位于存储它们的外部位置。Eric 值类型的标识仅来自存储 Eric 的变量。如果你把他传到处走,他就会失去他的身份。
19赞 ThunderGr 10/18/2013 #13

如果您曾经使用过 C/C++ 等语言进行编程,那么结构体可以用作可变结构。只要用 ref 传递它们,就没有什么可以出错的。我发现的唯一问题是 C# 编译器的限制,在某些情况下,我无法强制愚蠢的东西使用对结构的引用,而不是 Copy(例如当结构是 C# 类的一部分时)。

所以,可变结构不是邪恶的,C#使它们变得邪恶。我一直在 C++ 中使用可变结构,它们非常方便和直观。相比之下,C# 使我完全放弃了结构作为类成员,因为它们处理对象的方式。他们的便利使我们付出了代价。

评论

1赞 supercat 11/13/2013
拥有结构类型的类字段通常是一种非常有用的模式,尽管不可否认存在一些限制。如果使用属性而不是字段,或者使用 ,性能会降低,但如果避免做这些事情,结构类型的类字段就好了。结构的唯一真正基本的限制是,可变类类型的结构字段(如可以封装标识或一组不变的值),但不能用于封装可变值,而不封装不需要的标识。readonlyint[]
1赞 ShaunnyBwoy 12/19/2013 #14

如果使用得当,我不相信它们是邪恶的。我不会把它放在我的生产代码中,但我会用于结构化单元测试模拟之类的东西,其中结构的生命周期相对较短。

以 Eric 为例,也许您想创建该 Eric 的第二个实例,但要进行调整,因为这是测试的性质(即复制,然后修改)。如果我们只是在测试脚本的其余部分使用 Eric2,那么 Eric 的第一个实例会发生什么并不重要,除非您打算将他用作测试比较。

这对于测试或修改浅层定义特定对象(结构点)的遗留代码非常有用,但是通过具有不可变的结构,可以防止其烦人的使用。

评论

0赞 supercat 12/20/2013
在我看来,结构的核心是一堆用胶带粘在一起的变量。在 .NET 中,结构可以假装是一堆用胶带粘在一起的变量以外的东西,我建议在实际操作时,假装是一堆用胶带粘在一起的变量以外的类型应该表现为一个统一的对象(对于结构来说,这意味着不可变性), 但有时用胶带将一堆变量粘在一起很有用。即使在生产代码中,我也会认为最好有一个类型......
0赞 supercat 12/20/2013
...除了“每个字段都包含写入它的最后内容”之外,它显然没有语义,将所有语义推入使用该结构的代码中,而不是尝试让结构做更多的事情。例如,给定一个类型,其成员和字段类型为 ,以及 代码 ,任何关于 what 和 contain 的保证都应该来自 。作为一个暴露字段的结构体,可以清楚地表明它不会添加任何自己的行为。Range<T>MinimumMaximumTRange<double> myRange = foo.getRange();MinimumMaximumfoo.GetRange();Range
6赞 Ramesh Kadambi 5/13/2015 #15

埃里克·利珀特先生的例子有几个问题。它是为了说明结构被复制的观点,以及如果你不小心,这可能是一个问题。看看这个例子,我认为这是不良编程习惯的结果,而不是结构或类的真正问题。

  1. 结构应该只有公共成员,不需要任何封装。如果是这样,那么它确实应该是一个类型/类。你真的不需要两个结构来表达同样的事情。

  2. 如果类包含结构,则可以调用类中的方法来改变成员结构。这就是我作为一个良好的编程习惯会做的事情。

正确的实现方式如下。

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 按任意键继续。 。 。

评论

4赞 supercat 5/13/2015
将小型不透明结构设计为像不可变类对象一样的行为并没有错;当人们试图制作行为类似于对象的东西时,MSDN 指南是合理的。在某些情况下,当人们需要表现得像物体的轻量级东西时,以及需要用胶带粘在一起的一堆变量的情况下,结构是合适的。然而,由于某种原因,许多人没有意识到结构有两种不同的用法,并且适合其中一种的准则不适合另一种。
14赞 Luis Masuelli 12/2/2015 #16

如果你坚持结构的用途(在 C#、Visual Basic 6、Pascal/Delphi、C++ 结构类型(或类)中,当它们不用作指针时),你会发现结构只不过是一个复合变量。这意味着:您将把它们视为一组打包的变量,使用一个公用名(您从中引用成员的记录变量)。

我知道这会让很多深深习惯了 OOP 的人感到困惑,但这还不足以说明如果使用得当,这些东西本质上是邪恶的。有些结构是不可变的(Python 就是这种情况),但这是另一种需要考虑的范式。namedtuple

是的:结构体涉及大量内存,但执行以下操作不会恰恰是增加内存:

point.x = point.x + 1

与以下产品相比:

point = Point(point.x + 1, point.y)

内存消耗至少相同,甚至在不可变情况下甚至更多(尽管对于当前堆栈,这种情况是暂时的,具体取决于语言)。

但是,最后,结构是结构,而不是对象。在 POO 中,对象的主要属性是它们的身份,大多数时候它不超过它的内存地址。Struct 代表数据结构(不是一个合适的对象,因此它们无论如何都没有身份),并且可以修改数据。在其他语言中,record(而不是 struct,就像 Pascal 的情况一样)是这个词,并且具有相同的目的:只是一个数据记录变量,旨在从文件中读取、修改和转储到文件中(这是主要用途,在许多语言中,您甚至可以在记录中定义数据对齐方式,而正确称为 Objects 不一定是这种情况)。

想要一个好榜样吗?结构用于轻松读取文件。Python 之所以有这个库,是因为它是面向对象的,不支持结构,所以它必须以另一种方式实现它,这有点丑陋。实现结构的语言具有该特性...内置。尝试使用 Pascal 或 C 等语言的适当结构读取位图标头。这将很容易(如果结构体被正确构建和对齐;在Pascal中,你不会使用基于记录的访问,而是使用函数来读取任意二进制数据)。因此,对于文件和直接(本地)内存访问,结构比对象更好。至于今天,我们已经习惯了 JSON 和 XML,因此我们忘记了二进制文件的使用(以及副作用,使用结构)。但是,是的:它们存在,并且有目的。

他们不是邪恶的。只需将它们用于正确的目的。

如果你从锤子的角度来思考,你会想把螺丝当钉子,发现螺丝更难墙里,这将是螺丝的错,它们将是邪恶的。