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

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


当前回答

可变结构体并不邪恶。

在高绩效环境下,它们是绝对必要的。例如,当缓存线和垃圾收集成为瓶颈时。

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

我可以看到c#的语法没有帮助区分值类型或引用类型的成员的访问,所以我完全赞成使用强制不变性的不可变结构,而不是可变结构。

然而,与其简单地给不可变结构贴上“邪恶”的标签,我建议接受这种语言,提倡更有帮助和建设性的经验法则。

例如:"struct是默认复制的值类型。如果你不想复制他们,你需要一份推荐信。 “首先尝试使用只读结构体”。

其他回答

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

不可变值类型和只读字段

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

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

如果你曾经用C/ c++这样的语言编程,结构体可以作为可变的。只要把球传给裁判,没有什么会出错的。我发现的唯一问题是c#编译器的限制,在某些情况下,我无法强迫这个愚蠢的东西使用对结构的引用,而不是Copy(比如当结构是c#类的一部分时)。

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

我不会说可变性是邪恶的,但可变性通常是程序员急于提供最大功能的标志。在现实中,这通常是不需要的,反过来,使界面更小,更容易使用,更难使用错误(=更健壮)。

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

另外,我认为可变性几乎从来都不是真正需要的,程序员只是认为它在未来可能会出现。例如,改变日期是没有意义的。相反,在旧日期的基础上创建一个新的日期。这是一个廉价的操作,所以性能不是一个考虑因素。

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

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

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

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

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

Hugo

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

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

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