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


485

在SO上的讨论之后,我已经读过好几次关于可变结构是“邪恶的”的评论(就像在对这个问题的回答中)。

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


12
声明可变结构是邪恶的,就像声明可变ints,bools和所有其他值类型都是邪恶的一样。有可变性和不变性的情况。这些情况取决于数据扮演的角色,而不是取决于内存分配/共享的类型。
斯利普·汤普森

41
@slipp intbool不是可变..
Blorgbeard超出

2
…- .语法,即使引用类型的数据和值类型的数据截然不同,也使它们看起来相同。这是C#属性而不是结构的错误-某些语言提供了a[V][X] = 3.14用于就地变异的备用语法。在C#中,最好提供诸如'MutateV(Action <ref Vector2> mutator)'之类的struct-member mutator方法,并像这样使用它a.MutateV((v) => { v.X = 3; }) (示例由于C#对ref关键字的限制而被过度简化了,但是有些解决方法应该是可能的)
Slipp D. Thompson,2015年

2
@Slipp好吧,我认为这类结构正好相反。您为什么认为已经在.NET库中实现的结构(如DateTime或TimeSpan(如此类似))是不可变的?也许只更改这种struct的var中的一个成员可能会很有用,但这太不方便了,导致了太多的问题。实际上,您对处理器的计算结果是错误的,因为C#不会编译为汇编程序,而是会编译为IL。在IL(提供我们已经拥有了可变命名x)这个单一的操作是4个指令:ldloc.0(负载0指数可变进...
Sushi271

1
...类型 T是类型。Ref只是使变量本身传递给方法的关键字,而不是其副本。对于引用类型,它也是有意义的,因为我们可以更改变量,即方法外的引用在方法内更改后将指向其他对象。由于ref T不是类型,而是传递方法参数的方式,因此无法将其放入中<>,因为只能将类型放入其中。因此,这是不正确的。这样做可能会很方便,也许C#团队可以为某些新版本创建此代码,但现在他们正在开发某些版本……
Sushi271 2015年

Answers:


290

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

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

如果您的结构是不可变的,那么所有通过值传递而产生的自动副本将是相同的。

如果要更改它,则必须通过使用修改后的数据创建该结构的新实例来有意识地做到这一点。(不是副本)


82
“如果您的结构是不可变的,那么所有副本都是相同的。” 不,这意味着如果您想使用其他值,则必须自觉进行复制。这意味着您不会因为认为您正在修改原件而被发现修改副本。
卢卡斯

19
@Lucas我认为您正在谈论的是另一种副本,我是在谈论由于价值传递而产生的自动副本,您的“有意识地制作的副本”是有目的的,您故意没有犯错,不是真正的副本,它是故意包含不同数据的新瞬间。
蹦床

3
您的修改(16个月后)使这一点更加清晰。不过,我仍然坚持“((不可变的结构)意味着您不会以自己正在修改原始文件的方式来复制副本”)。
卢卡斯2010年

6
@Lucas:复制结构,修改结构并以某种方式认为修改原始结构的危险(当一个人在写一个struct字段的事实使人们很明显一个人仅在复制一个副本的事实)似乎很危险与拥有类对象作为持有其中包含的信息的手段的人将使该对象变异以更新其自身信息并在此过程中破坏其他对象所拥有的信息的危险相比,这种危险很小。
超级猫

2
第三段充其量听起来是错误的或不清楚的。如果您的结构是不可变的,那么您将无法修改其字段或任何已复制副本的字段。“如果你想改变它,你必须......”多数民众赞成误导过,你不能改变 以往任何时候,无论是自觉不自觉地也没有。创建一个新实例,该实例所需的数据与原始副本无关,但具有相同的数据结构。
Saeb Amini

167

从哪里开始;-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;

不太重要的是,存在大小问题;可变对象往往具有多种属性;但是,如果您的结构包含两个int,a string,a DateTime和a bool,则可以非常快速地消耗大量内存。使用一个类,多个调用者可以共享对同一实例的引用(引用很小)。


5
是的,但是编译器只是那样愚蠢。不要让分配财产结构成员是恕我直言,一个愚蠢的设计决策,因为它允许++运营商。在这种情况下,编译器只是自己编写显式分配,而不是忙于程序员。
康拉德·鲁道夫

12
@Konrad:myObj.SomeProperty.Size = 22将修改myObj.SomeProperty的COPY。编译器使您免于明显的错误。++不允许。
卢卡斯

@Lucas:好吧,Mono C#编译器肯定允许它–因为我没有Windows,所以我无法检查Microsoft的编译器。
Konrad Rudolph

6
@Konrad-少一个间接作用就可以了;正是这种情况“阻止了某些东西的值发生变化,该值仅作为临时值存在于堆栈上,并且将要蒸发成空”。
马克·格雷夫

2
@Marc Gravell:在前一段代码中,您最终得到一个名为“ abc”的“ Foo”,其其他属性是List [0]的那些属性,而不会干扰List [0]。如果Foo是一门课程,则有必要将其克隆然后更改副本。在我看来,值类型与类区别的最大问题是使用“”。运算符有两个目的。如果我有德鲁特,班级可以同时支持“。”。和“->”表示方法和属性,但“。”的常规语义是。属性将是创建一个具有适当字段修改的新实例。
超级猫

71

我不会说邪恶,但是可变性通常是程序员过度渴望提供最大功能的标志。实际上,通常不需要这样做,从而使接口更小,更易于使用且更难以使用错误(=更可靠)。

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

另外,我声称几乎从未真正需要可变性,程序员只是认为可能会在将来出现。例如,更改日期根本没有意义。而是根据旧日期创建一个新日期。这是一种廉价的操作,因此性能不是一个考虑因素。


1
埃里克·利珀特(Eric Lippert)说,他们是...看我的回答。
马克·格雷韦尔

42
我非常尊重埃里克·利珀特(Eric Lippert),他不是上帝(或者至少不是上帝)。当然,您链接到的博客文章以及您上面的文章是使结构不可变的合理论据,但实际上,它们对于使用可变结构的论点实际上是很薄弱的。但是,该帖子是+1。
Stephen Martin

2
使用C#进行开发时,通常时不时需要可变性-特别是在您的业务模型中,您希望在其中进行流传输等以与现有解决方案一起平稳地工作。我写了一篇关于如何使用可变的和不可改变的数据的工作,解决了周围的可变性大部分问题(我希望)的文章:rickyhelgesson.wordpress.com/2012/07/17/...
瑞奇Helgesson

3
@StephenMartin:封装单个值的结构通常应该是不可变的,但是到目前为止,结构是封装独立但相关变量(例如点的X和Y坐标)的固定集合的最佳媒介,这些变量不具有“同一性”组。用于目的的结构通常应将其变量公开为公共字段。我认为,出于这种目的,使用类比使用结构更合适,这是完全错误的。不可变的类通常效率较低,而可变的类通常具有可怕的语义。
2013年

2
@StephenMartin:例如,考虑一种应该返回float图形转换的六个组成部分的方法或属性。如果这样的方法返回具有六个组件的暴露场结构,则很显然,修改该结构的字段不会修改从其接收到的图形对象。如果这样的方法返回一个可变的类对象,则可能更改其属性将更改基础的图形对象,而可能不会—没人真正知道。
超级猫

48

可变结构不是邪恶的。

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

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

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

但是,我不建议将不可变的结构简单地标记为“邪恶的”,我建议您使用该语言,并提倡更有用和更具建设性的经验法则。

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


8
我还要假设,如果要将固定变量集与胶带固定在一起,以便可以分别或作为一个单元来处理或存储它们的值,那么要求编译器固定一组固定变量就更有意义了。变量在一起(即struct用公共字段声明a ),而不是定义一个可以笨拙地用于实现相同目的的类,或在结构中添加一堆垃圾以使其模仿此类(而不是使用它)表现得像一组用胶带粘在一起的变量,这是人们最初真正想要的)
supercat

41

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

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

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

struct PointyStruct {public int x,y,z;};
class PointyClass {public int x,y,z;};

void Method1(PointyStruct foo);
无效的Method2(ref PointyStruct foo);
void Method3(PointyClass foo);

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

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

答案:

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

Method1无法修改foo,也永远不会获得引用。Method2获得了对foo的短暂引用,它可以使用它以任意顺序多次修改foo的字段,直到返回为止,但是它不能持久保存该引用。在Method2返回之前,除非它使用了不安全的代码,否则可能已经完全删除了其“ foo”引用所创建的所有副本。与Method2不同,Method3获得了对foo的混杂共享引用,并且没有告诉它可能对它做什么。它可能根本不会更改foo,它可能会更改foo然后返回,或者它可能将foo的引用提供给另一个线程,该线程可能会在任意将来的某个时间以某种方式对其进行更改。

结构数组提供了奇妙的语义。给定Rectangle类型的RectArray [500],很明显地知道如何将元素123复制到元素456,然后在一段时间后将元素123的宽度设置为555,而不会干扰元素456。“ RectArray [432] = RectArray [321] ]; ...; RectArray [123] .Width = 555;“。知道Rectangle是具有称为Width的整数字段的结构,将告诉所有人都需要了解上述语句。

现在假设RectClass是一个与Rectangle具有相同字段的类,并且想要对RectClass类型的RectClassArray [500]执行相同的操作。也许该数组应该包含对可变的RectClass对象的500个预初始化的不可变引用。在这种情况下,正确的代码应类似于“ RectClassArray [321] .SetBounds(RectClassArray [456]); ...; RectClassArray [321] .X = 555;”。也许假设该数组包含不会更改的实例,所以正确的代码将更像是“ RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass(RectClassArray [321] ]); RectClassArray [321] .X = 555;“ 要知道应该做什么,就必须对RectClass有更多了解(例如,它是否支持复制构造函数,copy-from方法等)。)和数组的预期用途。没有比使用结构干净的地方了。

可以肯定的是,不幸的是,除了数组之外,任何容器类都没有一种好的方法来提供struct数组的清晰语义。如果一个人希望用一个字符串为一个集合建立索引,那么最好的办法就是提供一个通用的“ ActOnItem”方法,该方法将接受索引的字符串,一个通用参数以及一个将被传递的委托。通过引用通用参数和收集项。这将允许几乎与struct数组相同的语义,但是除非能够使vb.net和C#人员提供良好的语法,否则即使性能合理,代码也将显得笨拙(通过通用参数会允许使用静态委托,并且无需创建任何临时类实例)。

就个人而言,我很讨厌仇恨的埃里克·利珀特(Eric Lippert)等人。关于可变值类型。与在各处使用的混杂引用类型相比,它们提供了更简洁的语义。尽管.net支持值类型存在一些限制,但在许多情况下,可变值类型比任何其他类型的实体更适合。


1
@罗恩·沃霍里奇(Ron Warholic):SomeRect是矩形并不是显而易见的。它可以是其他一些类型,可以从Rectangle隐式地进行类型转换。虽然,唯一可以从Rectangle隐式类型转换的系统定义的类型是RectangleF,并且如果尝试将RectangleF的字段传递给Rectangle的构造函数,则编译器会发出嘶哑的声音(因为前者是Single,后者是Integer) ,可能存在允许此类隐式类型转换的用户定义结构。顺便说一句,无论SomeRect是Rectangle还是RectangleF,第一条语句都可以很好地工作。
supercat

您所显示的只是在一个人为的示例中,您认为一种方法更加清晰。如果我们以您的榜样为例,那么Rectangle我很容易想到一个共同点,那就是您的行为非常不清楚。考虑WinForms实现Rectangle在窗体Bounds属性中使用的可变类型。如果我想更改范围,我想使用您的漂亮语法:form.Bounds.X = 10;但这恰好改变了表单上的任何内容(并生成了一个可爱的错误通知您)。不一致是编程的祸根,这就是为什么需要不变性的原因。
罗恩·沃霍里奇

3
@Ron Warholic:BTW,我能够说“form.Bounds.X = 10;” 并使其正常工作,但是系统没有提供任何干净的方法。与使用类的任何方法相比,将值类型的属性公开为接受回调的方法的约定可以提供更整洁,有效且可确认正确的代码。
超级猫

4
这个答案比一些最受好评的答案更具洞察力。对于可变值类型的论点依赖于混叠和突变混合使用时所发生的“期望”概念,这有点荒谬。无论如何,这是一件可怕的事情!
Eamon Nerbonne'5

2
@supercat:谁知道呢,也许他们正在为C#7谈论的ref-return功能可能涵盖了这个基础(我实际上没有详细研究它,但是从表面上看,听起来很相似)。
Eamon Nerbonne '16

24

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

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

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


好吧,它们至少应该代表不变的概念;-p
Marc Gravell

3
好吧,我想他们可以使System.Drawing.Point不可变,但是恕我直言这将是一个严重的设计错误。我认为点实际上是原型值类型,并且是可变的。除了真正的早期编程101初学者以外,他们对任何人都不会造成任何问题。
斯蒂芬·马丁

3
原则上,我认为要点也应该是不变的,但是如果使类型更难使用或不太优雅,那么当然也必须考虑。如果没有人愿意使用具有最好的原则的代码结构,那是没有意义的;)
Morten Christiansen,2009年

3
值类型对于表示简单的不可变概念很有用,但是外露字段结构是用于保留或传递相关但独立的较小固定值(例如点的坐标)的最佳类型。这种值类型的存储位置封装了其字段的值,而没有其他内容。相反,可变引用类型的存储位置可用于保持可变对象状态的目的,但也封装了存在于该对象上的整个宇宙中所有其他引用的标识。
超级猫

4
“值类型基本上代表了不变的概念”。 不,他们没有。值类型变量的最古老,最有用的应用之一是int迭代器,如果它是不可变的,则将完全无用。我认为您正在将“值类型”的编译器/运行时实现与“类型为值类型的变量”混为一谈,后者肯定对任何可能的值都是易变的。
Slipp D. Thompson

19

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

不可变的值类型和只读字段

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

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


5
如果.net语言具有更好的值类型支持,应该发生的情况是,除非明确声明这样做,否则应禁止struct方法对“ this”进行突变,并且应禁止将如此声明的方法以只读方式进行上下文。可变结构的数组提供了有用的语义,这些语义无法通过其他方式有效实现。
supercat

2
这些都是可变结构会引起非常细微问题的很好的例子。我不会期望任何这种行为。为什么数组给您引用,但接口给您值呢?除了实时值(这是我真正希望的)之外,我本来会想到,至少这是相反的方式:提供引用的接口;给出值的数组...
Dave Cousineau'Feb

@Sahuagin:不幸的是,没有接口可以公开引用的标准机制。.net有多种方法可以安全有效地完成这些操作(例如,通过定义一个包含a T[]和一个整数索引的特殊“ ArrayRef <T>”结构,并提供对type属性的访问ArrayRef<T>将被解释为访问适当的数组元素)[如果一个类想ArrayRef<T>为其他目的公开a ,它可以提供一种方法(而不是属性)来检索它]。不幸的是,没有这样的规定。
2012年

2
噢,我的...这使可变结构可恶!
2013年

1
我喜欢这个答案,因为它包含了非常明显的有价值的信息。但实际上,尽管如此,这并不是某些主张反对可变结构的论点。是的,正如埃里克(Eric)所说,我们在这里看到的是一个“绝望之坑”,但是这种绝望的根源不是可变性。绝望的根源是结构的自我变异方法。(至于为什么数组和列表的行为不同的原因是,一个数组基本上是一个计算内存地址的运算符,另一个数组是一个属性。通常,一旦您理解“引用”是一个地址,一切就变得清楚了。)
AnorZaken

18

如果您曾经使用C / C ++之类的语言进行编程,则可以将结构用作可变变量。只需通过ref将它们传递给周围,就不会出错。我发现的唯一问题是C#编译器的限制,在某些情况下,我无法强迫愚蠢的人使用对结构的引用,而不是对Copy的引用(例如,当结构是C#类的一部分时) )。

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


具有结构类型的类字段通常可能是一个非常有用的模式,尽管可以承认存在一些限制。如果使用属性而不是字段或使用readonly,则会降低性能,但是如果避免这样做,结构类型的类字段就可以了。结构的唯一真正的根本限制是,可变类类型的struct字段int[]可以封装身份或不变的值集,但不能用于封装可变值而不封装不想要的身份。
2013年

13

如果坚持使用特定的结构(在C#,Visual Basic 6,Pascal / Delphi,C ++结构类型(或类)不用作指针的情况下),则将发现结构不超过复合变量。这意味着:您将以通用名称(引用成员的记录变量)将它们视为打包的变量集。

我知道这会使很多人深深地熟悉OOP,但如果正确使用,这还不足以说这样的事情本来就是邪恶的。有些结构按预期是不变的(Python就是这种情况namedtuple),但这是要考虑的另一种范式。

是的:结构涉及很多内存,但是通过执行以下操作,它不会恰好是更多的内存:

point.x = point.x + 1

相比:

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

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

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

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

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

如果您以锤子的方式考虑,您将要把螺钉当作钉子,发现螺钉更难以掉入墙壁,这将是螺钉的错,而它们将是邪恶的。


12

假设您有一个1,000,000个结构的数组。每个表示股权的结构都由C#/ VB创建,例如bid_price,offer_price(也许是小数)等。

想象一下,该数组是在非托管堆中分配的一块内存中创建的,因此其他一些本机代码线程可以并发访问该数组(也许有些高性能的代码在做数学运算)。

想象一下,C#/ VB代码正在监听价格变化的市场动态,那么该代码可能必须访问数组的某些元素(以安全性为准),然后修改某些价格字段。

想象一下,每秒完成数万甚至数十万次。

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

雨果


3
的确如此,但是在这种情况下,使用结构的原因是这样做是由外部约束(这些约束由本机代码的使用)强加给应用程序设计的。关于这些对象的所有其他说明都表明它们显然应该是C#或VB.NET中的类。
乔恩·汉纳

6
我不确定为什么有人认为这些东西应该是类对象。如果所有数组插槽均填充有引用不同实例的引用,则使用类类型将对内存需求增加额外的十二或二十四个字节,并且对类对象引用数组的顺序访问比在对象上的顺序访问慢得多。一系列结构。
超级猫

9

当某些东西可以被突变时,它会获得一种认同感。

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.position,但是其中一种比另一种更为直观。同样,将Eric(作为参考)传递给他修改方法更为直观。提供一种方法的Eric克隆几乎总是令人惊讶。任何想要变异的人都Person必须记住要求参考,Person否则他们做错事。

如果将类型设为不可变,则问题就消除了;如果我不能修改eric,是否收到对我没有影响eric还是的克隆对影响eric。更一般而言,如果类型的所有可观察状态都保存在以下任一成员中,则可以安全地按值传递类型:

  • 一成不变的
  • 参考类型
  • 安全地传递价值

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

不可变的直观性Person取决于您要执行的操作。如果Person仅代表一个人的一组数据,就没有什么直觉可言。Person变量真正代表抽象,而不是对象。(在这种情况下,将其重命名为可能更合适PersonData。)Person实际上是在模拟一个人本身,那么即使您避免了认为自己正在修改的陷阱,不断创建和移动克隆的想法也是很愚蠢的。原本的。在这种情况下,简单地创建Person一个引用类型(即一个类)可能会更自然。

诚然,由于函数式编程教会了我们所有的东西都是不变的(没有人可以秘密地保留对他的引用eric并对其进行变异),所以这样做是有好处的,但是由于在OOP中这不是惯用的,所以与您一起工作的其他任何人仍然不直观码。


3
您关于身份的观点很好。可能值得注意的是,身份仅在多个引用存在于某个事物时才相关。如果foo在宇宙中的任何地方都拥有对其目标的唯一引用,并且没有任何东西捕获该对象的身份哈希值,那么mutation字段foo.X在语义上等同于foo指向一个新对象,该对象与之前引用的对象相同,但是具有X保持所需的值。使用类类型,通常很难知道某个对象是否存在多个引用,但是使用结构很容易:它们并不存在。
2013年

1
如果Thing是可变的类类型,Thing[] 则将封装对象身份(无论是否希望),除非可以确保没有Thing外部引用存在的数组中的任何对象都将被突变。如果不希望数组元素封装身份,则通常必须确保对其持有引用的项目不进行任何突变,或者对其持有的任何项目都不存在外部引用[混合方法也可以使用]。两种方法都不是十分方便。如果Thing为结构,则Thing[]仅封装值。
2013年

对于对象,其身份来自于其位置。引用类型的实例具有其身份,这要归功于它们在内存中的位置,而您只传递它们的身份(引用),而不传递它们的数据,而值类型则在其外部存储它们的身份。Eric值类型的标识仅来自存储他的变量。如果您绕过他,他将失去他的身份。
IllidanS4希望莫妮卡回到2014年

6

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


3
如果您也将类用作映射中的键,则为true。
马克·格雷韦尔

6

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

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

而不是简单

data.value ++; 或data.value = data.value + 1;

在传递类时,数据封装很有用,并且您想确保以受控方式修改值。但是,当您拥有公共设置并获得的功能仅是将值设置为所传递的值时,与仅传递公共数据结构相比,这有何改进?

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

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


1
开门见山!结构是不受访问控制限制的组织单位!不幸的是,C#为此使它们毫无用处!
ThunderGr

完全错了要点,因为您的两个示例都显示了可变结构。
vidstige

C#使它们无用,因为这不是结构的目的
Luiz Felipe

6

埃里克·利珀特先生的例子有几个问题。它旨在说明复制结构的要点以及如果不注意的话可能会成为问题。查看示例,我发现它是由于不良的编程习惯而不是struct或class的问题所致。

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

看起来这是编程习惯的问题,而不是struct本身的问题。结构应该是可变的,这就是想法和意图。

更改的结果voila的行为符合预期:

1 2 3按任意键继续。。。


4
设计小的不透明结构使其表现为不可变的类对象没有错。当人们试图做行为像一个对象时,MSDN准则是合理。在某些情况下,结构是合适的,在这种情况下,人们需要像对象一样轻便的东西;在某些情况下,需要一堆用胶带粘在一起的变量。但是,由于某些原因,许多人未能意识到结构具有两种截然不同的用法,并且适合于一种的指南不适用于另一种。
2015年

5

可变数据有很多优点和缺点。百万美元的劣势是混叠。如果在多个位置使用了相同的值,并且其中一个更改了它,那么它似乎已经神奇地更改为正在使用它的其他位置。这与比赛条件有关,但并不完全相同。

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

口译员的技巧对这些折衷进行了详细介绍,并给出了一些示例。


在C#中,结构不是别名。每个结构分配都是一个副本。
递归

@recursive:在某些情况下,这是可变结构的主要优点,这使我质疑结构不应可变的概念。编译器有时会隐式复制结构这一事实并不会降低可变结构的有用性。
supercat

1

我不认为正确使用它们是有害的。我不会将其放入生产代码中,但会使用结构化单元测试模拟之类的东西,其中结构的寿命相对较小。

以Eric为例,也许您想创建该Eric的第二个实例,但要进行调整,因为这就是测试的本质(即重复,然后修改)。如果我们仅将Eric2用于测试脚本的其余部分,那么对于Eric的第一个实例发生什么都没有关系,除非您打算将其用作测试比较。

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


如我所见,结构的核心是一堆变量,它们通过胶带粘在一起。在.NET中,结构可能假装成不是用胶带粘在一起的一堆变量,而我建议当实际使用时,类型假装成不是一堆变量的东西。使用胶带的人应该表现为一个统一的对象(对于一个结构而言,这意味着不可变),但有时将一堆变量与胶带粘在一起会很有用。即使在生产代码中,我也认为最好有一个类型……
supercat

...显然没有任何语义超出“每个字段都包含写入它的最后一件事”,将所有语义推入使用该结构的代码中,而不是试图让结构做更多的事情。例如,给定Range<T>具有成员MinimumMaximum类型为type的字段T以及代码的类型,则Range<double> myRange = foo.getRange();关于什么MinimumMaximum包含的任何保证都应来自foo.GetRange();。其Range是暴露的场结构将清楚地表明它不会增加任何自己的行为。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.