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

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


当前回答

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

假设数组是在非托管堆中分配的内存块中创建的,以便其他一些本地代码线程能够并发地访问该数组(可能是一些高性能代码进行数学运算)。

想象一下c# /VB代码正在监听价格变化的市场反馈,该代码可能必须访问数组的某些元素(用于任何安全性),然后修改一些价格字段。

想象一下,这个过程以每秒数万次甚至数十万次的速度进行。

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

Hugo

其他回答

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

假设数组是在非托管堆中分配的内存块中创建的,以便其他一些本地代码线程能够并发地访问该数组(可能是一些高性能代码进行数学运算)。

想象一下c# /VB代码正在监听价格变化的市场反馈,该代码可能必须访问数组的某些元素(用于任何安全性),然后修改一些价格字段。

想象一下,这个过程以每秒数万次甚至数十万次的速度进行。

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

Hugo

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

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

这对于测试或修改那些浅层定义特定对象(结构体的重点)的遗留代码非常有用,但是通过使用不可变的结构体,可以避免令人讨厌的使用。

可变数据有许多优点和缺点。最大的缺点就是别名。如果相同的值在多个地方使用,其中一个地方更改了它,那么它将神奇地更改到正在使用它的其他地方。这与竞态条件有关,但并不完全相同。

有时候,价值百万美元的优势是模块化。可变状态允许您向代码隐藏更改的信息,而代码不需要知道这些信息。

《解释器的艺术》详细讨论了这些权衡,并给出了一些例子。

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

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

Because Person is mutable, it's more natural to think about changing Eric's position than cloning Eric, moving the clone, and destroying the original. Both operations would succeed in changing the contents of eric.position, but one is more intuitive than the other. Likewise, it's more intuitive to pass Eric around (as a reference) for methods to modify him. Giving a method a clone of Eric is almost always going to be surprising. Anyone wanting to mutate Person must remember to ask for a reference to Person or they'll be doing the wrong thing.

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

不可变的 引用类型 安全通过价值

如果满足这些条件,那么可变值类型的行为就像引用类型一样,因为浅拷贝仍然允许接收方修改原始数据。

The intuitiveness of an immutable Person depends on what you're trying to do though. If Person just represents a set of data about a person, there's nothing unintuitive about it; Person variables truly represent abstract values, not objects. (In that case, it'd probably be more appropriate to rename it to PersonData.) If Person is actually modeling a person itself, the idea of constantly creating and moving clones is silly even if you've avoided the pitfall of thinking you're modifying the original. In that case it'd probably be more natural to simply make Person a reference type (that is, a class.)

诚然,函数式编程已经告诉我们,使所有东西都不可变是有好处的(没有人可以秘密地保留对eric的引用并改变他),但由于这在OOP中不是惯用的,因此对于使用您的代码的其他人来说仍然是不直观的。

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

不可变值类型和只读字段

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

可变值类型和数组

假设我们有一个可变结构的数组,我们为该数组的第一个元素调用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);

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