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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么可变结构“邪恶”?相关的知识,希望对你有一定的参考价值。

在这里讨论了SO之后我已经多次读过可变结构是“邪恶”的评论(就像在这个question的答案中)。

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

答案

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

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

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

如果要更改它,则必须通过使用修改后的数据创建结构的新实例来有意识地执行此操作。 (不是副本)

另一答案

如果您坚持使用什么结构(在C#,Visual Basic 6,Pascal / Delphi,C ++结构类型(或类)中它们不用作指针时),您会发现结构不超过复合变量。这意味着:您将在一个通用名称(引用成员的记录变量)下将它们视为一组压缩变量。

我知道这会让很多人习惯于OOP而感到困惑,但如果使用得当,这并不足以说明这些事情本来就是邪恶的。有些结构是不可改变的(这是Python的namedtuple的情况),但它是另一种需要考虑的范例。

是的:结构涉及大量内存,但通过执行以下操作并不会更多内存:

point.x = point.x + 1

相比:

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

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

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

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

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

如果你考虑锤子,你会想要把螺丝当作钉子,找到螺丝更难以插入墙壁,这将是螺丝的错,它们将是邪恶的。

另一答案

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

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,我接受ericeric的克隆是没有区别的。更一般地说,如果一个类型的所有可观察状态都保存在以下成员中,则可以安全地传递值:

  • 一成不变
  • 参考类型
  • 安全地通过价值

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

不可变的Person的直观性取决于你想要做的事情。如果Person只代表一组关于一个人的数据,那就没有什么不直观的了; Person变量真正代表抽象值,而不是对象。 (在这种情况下,将它重命名为PersonData可能更合适。)如果Person实际上是在为一个人自己建模,那么即使你已经避免了思考你的陷阱,不断创建和移动克隆的想法也是愚蠢的。重新修改原件。在这种情况下,简单地将Person作为引用类型(即类)可能更自然。

当然,正如函数式编程告诉我们的那样,让一切都变得不可变(没有人可以秘密地保持对eric的引用并且让他变异),但是因为在OOP中这不是惯用的,所以对于其他任何工作的人来说,它仍然是不直观的。你的代码。

另一答案

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

另一答案

就个人而言,当我查看代码时,以下内容对我来说非常笨重:

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

而不是简单

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

传递类时,数据封装很有用,并且您希望确保以受控方式修改值。但是,当你有公共设置和获取功能时,只需将值设置为传递的值,这比仅仅传递公共数据结构有什么改进?

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

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

另一答案

Eric Lippert先生的例子有几个问题。人为地说明了结构被复制的意义以及如果你不小心可能会出现问题。看一下这个例子,我认为它是一个糟糕的编程习惯,而不是结构或类的问题。

  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本身的问题。结构应该是可变的,这就是想法和意图。

变化的结果表现如预期:

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

另一答案

可变数据有许多优点和缺点。百万美元的劣势是走样。如果在多个地方使用相同的值,并且其中一个更改了它,那么它似乎会神奇地改变为使用它的其他地方。这与竞争条件有关,但不相同。

百万美元的优势有时是模块化的。可变状态允许您隐藏不需要了解的代码中的更改信息。

The Art of the Interpreter详细介绍了这些权衡,并给出了一些例子。

另一答案

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

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

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

另一答案

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

不太重要的是,存在尺寸问题;可变对象往往具有多个属性;然而,如果你有一个结构有两个ints,一个string,一个DateTime和一个bool,你可以很快地烧掉大量的记忆。对于类,多个调用者可以共享对同一实例的引用(引用很小)。

另一答案

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

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

Also, I claim that mutability is almost never actually needed,程序员只是认为它可能在未来。例如,更改日期根本没有意义。而是根据旧日期创建一个新日期。这是一种廉价的操作,因此性能不是考虑因素。

另一答案

可变结构不是邪恶的。

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

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

我可以看出C#的语法无法区分值类型或引用类型成员的访问,所以我更喜欢不可变结构,强制不可变结构,而不是可变结构。

然而,我不是简单地将不可变结构标记为“邪恶”,而是建议采用该语言并倡导更有帮助和建设性的经验法则。

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

另一答案

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

改变“this”的结构方法(与属性设置者不同)有点邪恶,只是因为.net不提供区分它们的方法。不改变“this”的struct方法即使在只读结构上也应该是可调用的,而不需要防御性复制。修改“this”的方法在只读结构上根本不应该是可调用的。由于.net不希望禁止不修改“this”的struct方法在只读结构上调用,但不希望允许只读结构发生变异,因此它会在读取时防御性地复制结构唯一的背景,可以说是两个世界中最糟糕的。

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

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: Method1():没有(明确的意图) Method2():是的(明确意图) Method3():是的(不确定的意图) 问题2: 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]; ...; RectArray [123] .Width = 555;” 。知道Rectangle是一个带有名为Width的整数字段的结构,它将告诉所有人需要知道上述语句。

现在假设RectClass是一个与Rectangle具有相同字段的类,并且想要对RectClass类型的RectClassArray [500]执行相同的操作。也许该数组应该包含500个预先初始化的可变引用,这些引用是可变的RectClass

以上是关于为什么可变结构“邪恶”?的主要内容,如果未能解决你的问题,请参考以下文章

代码片段 PHP,预期文件结尾,我错在哪里?

返回C ++引用变量的做法是邪恶的吗?

什么时候 eval 在 php 中是邪恶的?

函数指针是邪恶的吗? [关闭]

为啥 cmake 文件 GLOB 是邪恶的?

代码崩溃编译器:main() 返回结构而不是 int