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

554

在 Stack Overflow 上的讨论中,我已经多次看到关于可变结构体是“邪恶”的评论(例如这个问题的答案中)。

在 C# 中,可变性和结构体有什么实际问题?


24
声称可变结构体是邪恶的就像声称可变的int、bool和所有其他值类型都是邪恶的一样。在可变性和不可变性之间存在情况,这些情况取决于数据扮演的角色,而不是内存分配/共享的类型。 - Slipp D. Thompson
63
@slipp中的intbool类型是不可变的。 - Blorgbeard
2
在C#中,属性的问题导致了“.”语法,使得使用引用类型数据和值类型数据进行操作看起来相同,尽管它们是明显不同的。这不是结构体的问题,而是C#属性的问题——一些语言提供了一种替代的a[V][X] = 3.14语法来进行原地变异。在C#中,你最好提供结构体成员变异器方法,比如'MutateV(Action<ref Vector2> mutator),并像这样使用它:a.MutateV((v) => { v.X = 3; })(由于C#关键字ref`的限制,示例过于简化,但通过一些解决方法应该是可能的)。 - Slipp D. Thompson
3
@Slipp,我对这种类型的结构体持完全相反的看法。为什么您认为已在 .NET 库中实现的结构体,例如 DateTime 或 TimeSpan(类似的结构体),是不可变的?也许更改这种结构体变量的一个成员可能是有用的,但这样做太不方便了,会导致太多问题。实际上,您关于处理器计算的观点是错误的,因为 C# 并不编译成汇编语言,而是编译成 IL(Intermediate Language,中间语言)。在 IL 中(假设我们已经有名为“x”的变量),这个单一操作需要 4 条指令:“ldloc.0”(将 0 索引变量加载到... - Sushi271
1
...更重要的事情,比如空值条件运算符或自动属性初始化器。至于“public”...我实际上很喜欢C#的做法。这样做实际上更容易重构,因为在一个成员的一个地方删除/添加一个公共成员不会影响所有其他成员。在C++中,我经常不得不添加两个标签(例如public:和private:),或将方法移动到类的其他部分。不方便。无论如何,提到这个话题让我意识到我们应该结束那个讨论。我们开始争论我们自己的观点,我们可能会争论多年而找不到共同点... - Sushi271
显示剩余7条评论
16个回答

326

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

因此,如果您更改的是副本,则仅更改该副本,而不是原始副本或其他可能存在的副本。

如果您的结构体是不可变的,则由于按值传递而自动生成的所有副本将是相同的。

如果你想要修改它,你必须有意识地使用修改后的数据创建一个新的结构体实例。(而不是复制一份)


95
如果你的结构体是不可变的,那么所有的副本都是相同的。但这并不意味着你不能得到不同的值,你需要有意识地制作一个副本。这意味着你不会因为修改一个副本而误以为你在修改原始数据。 - Lucas
27
@Lucas 我认为你谈论的是另一种类型的复制,我谈论的是由于传值而自动生成的自动复制。你所提到的“有意识的复制”是故意创建的,不是出于错误而产生的,并且它并不是真正的复制,而是包含不同数据的全新实例。 - trampster
6
你的编辑 (16个月后) 使其更加清晰。不过,我依然认为 "(immutable struct) 意味着你不会因为修改副本而认为你在修改原始内容"。 - Lucas
7
对于复制一个结构体并对其进行修改,然后错误地认为自己在修改原始结构体的危险性(实际上只是在修改自己的副本),相较于某个持有类对象以存储其中包含信息并将其突变以更新其自身信息并在此过程中破坏其他对象所持有的信息的危险性来说,似乎要小得多。 - supercat
6
第三段听起来最好是错误的或者说不清楚。如果你的结构是不可变的,那么你就无法修改它的任何字段或任何副本的字段。 “如果你想要改变它,你必须……” 这也是误导性的,你根本无法改变,无论是有意识地还是无意识地。你需要创建一个新实例,其中所需的数据与原始副本除了具有相同的数据结构外没有任何关系。 - Saeb Amini
显示剩余4条评论

181

从哪里开始呢 ;-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、一个 string、一个 DateTime 和一个 bool 的结构体,那么您很快就会消耗大量内存。使用类时,多个调用者可以共享对同一实例的引用(引用很小)。


6
是的,但编译器就是这样傻。不允许对属性结构成员进行赋值,在我看来是一个愚蠢的设计决策,因为对于 ++ 运算符是允许的。在这种情况下,编译器会自己写出明确的赋值,而不是让程序员费心。 - Konrad Rudolph
14
@Konrad:myObj.SomeProperty.Size = 22 会修改 myObj.SomeProperty 的一个副本。编译器正在防止你犯一个明显的错误。而且 ++ 不被允许。 - Lucas
7
@Konrad - 如果减少一层间接引用,它应该可以工作;被阻止的情况是“改变一个仅存在于堆栈上的瞬态值的值,并且该值即将消失”的情况。 - Marc Gravell
2
@Marc Gravell:在前面的代码片段中,你最终得到一个名为"abc"的"Foo",其其他属性与List[0]相同,而不会影响List[0]。如果Foo是一个类,那么就需要克隆它然后修改副本。在我看来,值类型和类的区别的一个大问题是使用"."运算符有两个目的。如果我可以选择的话,类应该支持方法和属性的"."和"->"两种操作符,但是对于"."属性的正常语义应该是创建一个新实例并修改相应的字段。 - supercat
1
@Backwards_Dave,你可能正在比较不同的情况;要么SomeProperty实际上不是属性(也许它是一个字段?),要么SomeProperty的类型实际上不是struct。下面是一个最小化的重现,显示了CS1612:https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQxASwDYB8ACAGAAhwEYBuAWACgcBmIgJgIGECBvKgzougZQHswcHjAQBXAMYwC/QQAUEfAA6IYATzYBzODFJRtpAL4cutIgBYCAWQAUASjbGuXGXHlKVqgHQ80ALzgEALwE9HgUlE5GlFFUprBiktICQiISUuwRJnRoAHZSPv7hBkA= - Marc Gravell
显示剩余10条评论

80

我不会说邪恶,但是可变性通常是程序员过于热衷于提供最大功能的标志。实际上,这通常是不必要的,反过来使得接口更小,更容易使用,更难出错(=更健壮)。

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

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


1
Eric Lippert说他们是...请看我的回答。 - Marc Gravell
50
虽然我很尊重Eric Lippert,但他并不是上帝(至少目前还不是)。你提供的博客链接和你上面的文章都有合理的论点支持将结构体默认定义为不可变的,但它们并不足以成为永远不使用可变结构体的论据。然而,这篇文章非常棒。 - Stephen Martin
2
在C#中开发,通常需要时不时地使用可变性 - 特别是在您的业务模型中,您希望流等功能与现有解决方案平稳工作。我写了一篇关于如何处理可变和不可变数据的文章,解决了大多数可变性问题(我希望):http://rickyhelgesson.wordpress.com/2012/07/17/mutable-or-immutable-in-a-parallel-world/ - Ricky Helgesson
4
结构体通常应该是不可变的,当它们封装单个值时。但是,结构体远远是封装独立但相关变量(例如点的X和Y坐标)的固定集合的最佳媒介,并且这些变量作为一组没有“身份”的存在。对于用于这种目的的结构体,通常应将其变量公开为公共字段。认为在此类情况下更适合使用类而不是结构体的观点,我认为是完全错误的。不可变类通常效率较低,可变类通常具有可怕的语义。 - supercat
3
考虑一个方法或属性,它应该返回图形变换的六个float组件。如果这种方法返回一个包含六个组件的暴露字段结构体,显然修改结构体的字段不会修改它所接收自的图形对象。如果这种方法返回一个可变类对象,也许改变它的属性将改变底层的图形对象,也许不会——没有人真正知道。 - supercat
显示剩余6条评论

66

可变结构体并非邪恶。

在高性能环境中,它们是绝对必需的。例如,当缓存行或垃圾回收成为瓶颈时。

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

我可以理解C#语法不能帮助区分值类型成员和引用类型成员的访问,因此我完全支持更喜欢强制执行不可变性的不可变结构体。

然而,与其简单地将不可变结构体标记为“邪恶”,我建议拥抱该语言并提倡更有帮助和建设性的原则。

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


11
我认为,如果一个人想要用胶带将一组固定的变量粘在一起,以便可以将它们的值分别或作为一个整体进行处理或存储,那么更有意义的做法是要求编译器将这组固定的变量绑定在一起(即声明一个具有公共字段的“结构体”),而不是定义一个类来笨拙地实现相同的目的,或者向结构体添加一堆垃圾以使其模拟这样的类(而不是让它像一组用胶带粘在一起的变量,这才是人们最初真正想要的)。 - supercat

50

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

与属性设置器不同的结构体方法会改变 "this",这有点邪恶,只是因为 .net 没有提供区分它们和不改变 "this" 的方法的手段。不改变 "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. 假设该方法不使用任何 "unsafe" 代码,它是否可能修改 foo?
  2. 如果在调用该方法之前不存在对 'foo' 的任何外部引用,那么调用后是否可能存在外部引用?

答案:

问题1:
Method1(): 不可以 (明确的意图)
Method2(): 可以 (明确的意图)
Method3(): 可以 (不确定的意图)
问题2:
Method1(): 不可以
Method2(): 不可以 (除非使用 unsafe)
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];…; RectArray[123].Width = 555;”。知道Rectangle是一个带有名为Width的整数字段的结构体就足以理解上述语句。

假设RectClass是一个具有与Rectangle相同字段的类,想要在类型为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(例如,它是否支持复制构造函数,复制-从方法等)和数组的预期使用了解得更多。远不如使用struct干净。

确保除数组以外的任何容器类都没有提供结构数组的干净语义的好方法。如果希望使用字符串索引集合,则最好的方法可能是提供一个通用的“ActOnItem”方法,该方法将接受索引的字符串,一般参数和一个委托,该委托将被传递通过引用传递通用参数和集合项。这将允许几乎与结构数组相同的语义,但除非vb.net和C#的人们能提供漂亮的语法,否则即使代码表现合理,代码也会看起来笨拙(传递通用参数将允许使用静态委托并避免创建任何临时类实例)。

个人而言,我对Eric Lippert等人关于可变值类型的憎恶感到恼怒。它们提供比滥用引用类型更干净的语义。尽管.net对值类型的支持存在一些限制,但在许多情况下,可变值类型都比任何其他类型的实体更适合。


1
@Ron Warholic:并不是显而易见的SomeRect是一个Rectangle。它可能是其他类型,可以从Rectangle隐式类型转换。虽然,唯一可以从Rectangle隐式类型转换的系统定义类型是RectangleF,如果尝试将RectangleF的字段传递给Rectangle的构造函数,编译器会发出警告(因为前者是Single,后者是Integer),但可能存在允许这种隐式类型转换的用户定义结构体。顺便说一句,第一条语句无论SomeRect是Rectangle还是RectangleF都能很好地工作。 - supercat
3
@Ron Warholic:顺便说一句,我希望能够说“form.Bounds.X = 10;”并使其正常工作,但是系统没有提供任何简洁的方法来实现。通过将值类型属性公开为接受回调的方法的约定,可以比使用类的任何方法提供更清晰、高效和可确认正确性的代码。 - supercat
5
这个答案比一些获得最高票数的答案更有深度。反对可变值类型的论点以“你期望发生什么”为基础,这种做法非常荒谬。不管怎样,通过混合别名和变异来操作对象都是一个糟糕的做法! - Eamon Nerbonne
@EamonNerbonne:我之前没有想到过那个措辞,但你说得完全正确,并且这也解释了为什么暴露字段结构通常是正确的方法:它们只支持短暂的别名。然而,很遗憾.NET语言没有一个好的模式来公开短暂的别名;我希望有一种模式可以使“foo.Bar(57).X=boz;”自动转换为“foo.access_Bar(57, (ref Point it, ref int p1) => it.X=p1, ref boz)”,从而允许除数组以外的集合访问其中包含的结构体的内部。 - supercat
2
@supercat:谁知道呢,也许C# 7中他们正在谈论的ref-return功能可以涵盖这个基础(我实际上还没有详细研究过它,但表面上听起来很相似)。 - Eamon Nerbonne
显示剩余6条评论

27

还有一些边角案例可能会导致程序员在视角上出现不可预测的行为。

不可变值类型和只读字段

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

因此,只要您和团队清楚地了解自己正在做什么,可变结构就不是邪恶的。但是,在太多角落情况下,程序行为会与预期不同,这可能会导致微妙且难以产生和理解的错误。


9
如果 .NET 语言的值类型支持稍微更好,那么应该发生的是,除非明确声明为这样做的情况下,结构体方法应禁止修改“this”,而被声明为这样做的方法则应在只读上下文中禁止。可变结构体数组提供了有用的语义,无法通过其他方式高效实现。 - supercat
2
这些都是非常微妙的问题,会因为可变结构体而出现。我从未预料到会有任何这样的行为。为什么数组会给你一个引用,但接口却给你一个值?除了一直使用值(这是我真正期望的),我本以为至少会相反:接口提供引用;数组提供值... - Dave Cousineau
@Sahuagin:不幸的是,没有标准机制可以使接口公开引用。虽然有办法让.net安全和有用地完成这样的事情(例如,通过定义一个特殊的“ArrayRef<T>”结构,其中包含T[]和整数索引,并提供将对类型为“ArrayRef<T>”的属性的访问解释为访问适当的数组元素),如果一个类想要为任何其他目的公开“ArrayRef<T>”,它可以提供一个方法-而不是属性-来检索它。不幸的是,目前没有这样的规定。 - supercat
2
哦天啊...这让可变结构体太邪恶了! - nawfal
1
当您将可变方法重构为需要 ref 参数的静态方法时:public static void IncrementI(ref Mutable m) { m.I++; },编译器应该在大多数情况下阻止您做“错误”的事情。 - springy76
2
我喜欢这个答案,因为它包含非常有价值的信息,这些信息并不明显。但是,实际上,这并不是反对可变结构体的论据,正如一些人所声称的那样。是的,我们在这里看到了一个“绝望之坑”,正如Eric所说的那样,但是这种绝望的根源并不是可变性。绝望的根源是结构体自我变异的方法。(至于为什么数组和列表的行为不同,这是因为一个基本上是计算内存地址的运算符,而另一个是属性。总的来说,一旦你理解了“引用”是一个地址,一切都会变得清晰明了。) - AnorZaken

23

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

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

希望这对您有所帮助。确保更多关注你尝试使用值类型捕获的概念,而不是实际细节。


4
我想他们本来可以使System.Drawing.Point成为不可变类型,但在我看来,那将是一个严重的设计错误。我认为点实际上是一个典型的值类型,并且它们是可变的。而且,除了真正初学编程的新手之外,它们对任何人都不会造成问题。 - Stephen Martin
4
原则上,我认为点也应该是不可变的,但如果这使得类型更难使用或不够优雅,那当然也必须考虑。如果没有人想使用代码结构来维护最好的原则,那么这些结构就没有意义 ;) - Morten Christiansen
4
值类型适用于表示简单的不可变概念,但暴露字段结构是最好的类型,用于保存或传递一组相关但独立的小固定值(例如点的坐标)。这样的值类型存储位置封装了其字段的值,除此之外没有其他任何内容。相比之下,可变引用类型的存储位置可以用于保存可变对象的状态,但也封装了整个宇宙中存在于同一对象上的所有其他引用的身份。 - supercat
6
“值类型基本上代表不可变的概念。” 这种说法是不正确的。其中一种最古老且最有用的值类型变量应用是 int 迭代器,如果它是不可变的,那将是完全无用的。我认为你混淆了“值类型的编译器/运行时实现”和“被赋予值类型的变量”,后者肯定可以被任何可能的值所改变。 - Slipp D. Thompson
1
根据您在此答案中所述的逻辑,所有类型都是不可变的。类被存储为值类型和引用(内存地址指针/句柄)的集合,因此它们也是不可变的,因为您不会更改内存地址,而只是“分配另一个唯一值”。问题显然是关于以在初始化后的某个时间更改它们所包含的值和内存位置的结构类别数据结构的拟议使用,从高级程序员的角度来看。将讨论转向编译器优化使这个A无关紧要。 - Slipp D. Thompson
显示剩余4条评论

19
如果您曾经使用过像C/C++这样的语言进行编程,那么结构体作为可变对象是很好用的。只需通过引用传递它们,就没有什么可以出错的了。我发现唯一的问题是C#编译器的限制,在某些情况下,我无法强制它使用结构体的引用,而不是副本(例如当结构体是C#类的一部分时)。
因此,可变的结构体并不是邪恶的,是C#让它们变得邪恶。我在C++中经常使用可变的结构体,它们非常方便和直观。相比之下,由于处理对象的方式,C#迫使我完全放弃将结构体作为类成员的使用。它们的方便性代价了我们自己的方便。

1
拥有结构类型的类字段通常是一种非常有用的模式,尽管必须承认存在一些限制。如果使用属性而不是字段或使用“readonly”,性能将会降低,但是如果避免这些问题,则结构类型的类字段就可以正常使用。结构体的唯一真正的基本限制是,像int[]这样的可变类类型的结构体字段可能会封装标识或不变的值集,但不能用于封装可变值而不同时封装不需要的标识。 - supercat

14
假设您有一个包含100万个结构的数组。每个结构表示一种股票,包括报价、报盘价格(可能是小数)等信息,这是由C#/VB创建的。
假设该数组在未托管堆中分配的内存块中创建,以便某些其他本机代码线程能够同时访问该数组(可能是做数学计算的高性能代码)。
想象一下,C#/VB代码正在监听价格变化的市场数据源,该代码可能需要访问数组的某个元素(用于任何一个证券),然后修改某些价格字段。
想象一下,这样做每秒钟要进行数以万计甚至几十万次。
那么,面对事实,我们确实希望这些结构可变,因为它们正在被一些其他本机代码共享,因此创建副本不会有所帮助;它们需要是可变的,因为以这种速率制作一份120字节结构的副本是疯狂的,特别是当更新可能实际上只影响1或2个字节时。

3
是的,但在这种情况下,使用结构体的原因是因为外部约束(本地代码的使用)强制应用程序设计使用它们。您所描述的这些对象的所有其他方面都表明它们应该在C#或VB.NET中明显成为类。 - Jon Hanna
7
我不确定为什么有些人认为这些东西应该是类对象。如果所有的数组槽都被填充了具有独特实例引用的实例,使用类类型将会增加额外的12或24个字节的内存需求,并且在类对象引用数组上的顺序访问很可能比在结构体数组上的顺序访问要慢得多。 - supercat

14
如果你遵循结构体的原始意图(在C#,Visual Basic 6,Pascal / Delphi,C ++结构类型(或类)在它们不用作指针时),你会发现结构体只是一个复合变量。这意味着:您将把它们视为一组紧凑的变量,具有共同的名称(您将从中引用成员的记录变量)。
我知道这可能会让很多习惯于面向对象编程的人感到困惑,但这并不足以说这种东西本质上是邪恶的,如果使用正确的话。有些结构体是不可变的,就像它们本来的意图一样(比如Python中的namedtuple),但这是另一种需要考虑的范例。
是的:结构体涉及大量内存,但这并不是通过执行以下操作而精确获得更多内存的方法:
point.x = point.x + 1

相对于:

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

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

但最终,结构体只是“结构体”,并非对象。在面向对象编程中,对象的主要属性是它们的“标识”,大多数情况下不超过其内存地址。结构体代表数据结构(不是一个适当的对象,因此它们无论如何都没有标识),数据可以被修改。在其他语言中,“记录”(而不是“结构体”,如Pascal所示)是这个词,并且具有相同的目的:只是一个数据记录变量,旨在从文件中读取,修改和转储到文件中(这是主要用途,在许多语言中,您甚至可以定义记录中的数据对齐方式,而对于适当称为对象的情况则不一定如此)。

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

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

如果您考虑钉子,您将希望将螺钉视为钉子,发现螺钉更难插入墙壁,这将是螺钉的错,它们将成为罪魁祸首。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接