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

时间:2009-01-13 23:27:48

标签: c# struct immutability mutable

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

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

16 个答案:

答案 0 :(得分:277)

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

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

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

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

答案 1 :(得分:164)

从何处开始;-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,一个string,一个DateTime和一个bool的结构,你可以很快地烧掉大量的内存。对于一个类,多个调用者可以共享对同一实例的引用(引用很小)。

答案 2 :(得分:68)

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

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

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

答案 3 :(得分:41)

可变结构不是邪恶的。

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

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

我可以看出C#的语法无法区分值类型或引用类型成员的访问权限,所以我全都是偏好不可变结构,强制执行不变性,在可变的结构上。

然而,我不是简单地将不可变结构标记为“邪恶”,而是建议接受这种语言并倡导更有帮助和建设性的拇指规则。

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

答案 4 :(得分:35)

答案 5 :(得分:22)

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

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

希望这对你有意义。更确切地说,它更多地是关于您尝试使用值类型而非实际细节捕获的概念。

答案 6 :(得分:17)

还有另外几个角落案例可能会导致程序员的不可预测行为。这里有几个。

  1. 不可变值类型和只读字段
  2. // 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);
        }
    }
    

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

    1. 可变值类型和数组
    2. 假设有一个Mutable结构的数组,我们正在为该数组的第一个元素调用IncrementI方法。你对这次电话的期望是什么?它应该更改数组的值还是仅更改副本?

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

答案 7 :(得分:15)

如果您曾使用C / C ++这样的语言编程,那么结构体可以用作可变的。只需用ref传递它们,周围就没有什么可以出错的。我发现的唯一问题是C#编译器的限制,在某些情况下,我无法强制使用对结构的引用而不是复制(就像结构是C#类的一部分时一样) )。

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

答案 8 :(得分:11)

想象一下,你有一个包含1,000,000个结构的数组。每个结构代表一个与bid_price,offer_price(可能是小数)等东西的股权,这是由C#/ VB创建的。

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

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

想象一下,每秒钟要做几十甚至几十万次。

嗯,让我们面对事实,在这种情况下,我们确实希望这些结构是可变的,它们需要是因为它们被其他一些本机代码共享,所以创建副本不会有帮助;他们需要做的是因为以这些速率制作一个120字节结构的副本是疯狂的,特别是当更新实际上只影响一两个字节时。

雨果

答案 9 :(得分:11)

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

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

是的:结构涉及大量内存,但实际上并不是更多的内存:

point.x = point.x + 1

与之相比:

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

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

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

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

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

如果你认为在锤子方面,你会想要把螺丝当作钉子,找到螺丝更难以插入墙壁,它将是螺丝钉。错,他们将是邪恶的。

答案 10 :(得分:7)

当某些事情发生变异时,它会获得一种认同感。

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中惯用它对于使用你的代码的任何其他人来说仍然是不直观的。

答案 11 :(得分:5)

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

答案 12 :(得分:5)

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

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

而不是简单

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

数据封装在传递类时很有用,并且您希望确保以受控方式修改值。但是,如果你有公共设置和获取的函数只是将值设置为传递的值,那么这比简单地传递公共数据结构有什么改进呢?

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

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

答案 13 :(得分:5)

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

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

  2. 如果你有一个封闭结构的类,你可以在类中调用一个方法来改变成员结构。这就是我作为一个良好的编程习惯所做的事情。

  3. 正确的实施方案如下。

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

答案 14 :(得分:4)

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

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

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

答案 15 :(得分:1)

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

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

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