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

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

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

98035 次浏览

从哪里开始;-p

埃里克·利珀特的博客总是很好的引用:

这是可变的另一个原因 值类型是邪恶的。试着总是 使值类型不可变

首先,您很容易丢失更改……例如,从列表中获取内容:

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;

不那么关键的是规模问题;可变对象往往具有多个属性;但是如果你的struct有两个# eyz0,一个string,一个DateTime和一个bool,你会很快消耗大量内存。使用类,多个调用方可以共享对同一个实例的引用(引用很小)。

它与结构无关(也与c#无关),但在Java中,当可变对象是哈希映射中的键时,你可能会遇到问题。如果你在将它们添加到映射后更改它们,并且它更改了散列码,可能会发生糟糕的事情。

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

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

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

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

所以如果你改变了一份副本,你只是改变了那份副本,而不是原件,也不是周围可能存在的其他副本。

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

如果你想要改变它,你必须有意识地用修改过的数据创建一个结构的新实例。(非副本)

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

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

《口译的艺术详细讨论了这些权衡,并给出了一些示例。

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

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

希望这能让你明白。可以肯定的是,它更多的是关于您试图用值类型捕获的概念,而不是实际的细节。

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

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

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

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

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

雨果

就我个人而言,当我看代码时,下面的代码看起来相当笨拙:

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

而不是简单地

数据。值+ +;或数据。Value = data。值+ 1;

数据封装在传递类时非常有用,并且您希望确保以受控的方式修改值。然而,当你拥有公共的set和get函数,它们所做的仅仅是将值设置为传递进来的值时,这比简单地传递公共数据结构有什么改进呢?

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

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

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

不可变值类型和只读字段

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

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

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

改变“this”的Struct方法(与属性设置器不同)有点邪恶,只是因为。net没有提供一种方法将它们与没有改变的方法区分开来。不改变“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. 假设该方法没有使用任何“不安全”代码,它可以修改foo吗?
  2. 如果在调用方法之前没有对'foo'的外部引用,那么在调用方法之后是否可以存在外部引用?

答案:

问题1:
Method1(): no (明确的意图)
Method2():是(明确的意图)
Method3():是(不确定的意图)
问题2:< br >  # EYZ0:没有< br >  Method2(): no (除非不安全)
 # EYZ0:是的< / p >

Method1不能修改foo,也永远得不到引用。Method2获得了对foo的短时间引用,它可以使用它以任何顺序修改foo的字段任意次数,直到返回,但它不能持久化该引用。在Method2返回之前,除非它使用了不安全的代码,否则它的'foo'引用的任何和所有副本都将消失。Method3,不像Method2,得到一个杂乱共享的foo引用,没有人知道它会用它做什么。它可能根本不改变foo,它可能改变foo然后返回,或者它可能给另一个线程一个对foo的引用,这可能在未来的任意时间以某种任意方式改变它。限制Method3对传递给它的可变类对象所做的事情的唯一方法是将可变对象封装到只读包装器中,这既丑陋又麻烦。

结构数组提供了美妙的语义。给定矩形类型的RectArray[500],如何将元素123复制到元素456,然后在不影响元素456的情况下,将元素123的宽度设置为555是显而易见的。"RectArray[432] = RectArray[321];…;RectArray[123]。宽度= 555;"。知道Rectangle是一个具有名为Width的整数字段的结构体,就可以知道关于上述语句的所有信息。

现在假设RectClass是一个与Rectangle具有相同字段的类,并且希望在类型为RectClass的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(例如,它是否支持复制构造函数,复制方法等)和数组的预期用法。远不及使用结构体简洁。

可以肯定的是,不幸的是,除了数组之外,任何容器类都没有更好的方法来提供结构数组的清晰语义。最好的方法是,如果你想要一个集合被索引,例如一个字符串,可能是提供一个通用的“ActOnItem”方法,它将接受一个字符串作为索引,一个通用参数,以及一个委托,它将通过引用传递通用参数和集合项。这将允许几乎相同的语义结构数组,但除非vb.net和c#的人可以提供一个良好的语法,即使它是合理的性能(传递一个泛型参数将允许使用静态委托,并将避免任何需要创建任何临时类实例),代码也将是笨拙的。

就我个人而言,我对Eric Lippert等人对可变值类型的憎恨感到恼火。它们提供了比到处使用的混杂引用类型清晰得多的语义。尽管.net对值类型的支持有一些限制,但在许多情况下,可变值类型比任何其他类型的实体都更适合。

可变结构体并不邪恶。

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

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

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

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

例如:结构体是值类型,默认情况下复制。如果你不想复制他们,你需要一份推荐信。”"试着先处理只读结构"

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

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.position的内容,但是其中一个操作比另一个更直观。同样,通过传递Eric(作为引用)来获取修改他的方法更直观。给一个方法一个Eric的克隆几乎总是令人惊讶的。任何想要突变Person的人都必须记住请求对Person的引用,否则他们会做错误的事情。

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

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

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

不可变的Person的直观程度取决于您要做什么。如果Person只是代表关于一个人的数据集,这并没有什么不直观的;Person变量真正代表抽象的,而不是对象。(在这种情况下,将其重命名为PersonData可能更合适。)如果Person实际上是在建模一个人本身,那么不断创建和移动克隆的想法是愚蠢的,即使您已经避免了认为您正在修改原始的陷阱。在这种情况下,简单地将Person作为引用类型(即类)可能会更自然。

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

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

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

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

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

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

李柏特先生举的例子有几个问题。它是为了说明结构是复制的,以及如果你不小心的话,这可能是一个问题。看看这个例子,我认为这是一个坏的编程习惯的结果,而不是结构或类的问题。

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

看起来这是编程习惯的问题,而不是结构本身的问题。结构体应该是可变的,这是它的思想和意图。

更改的结果voila表现如预期:

< p > 1 2 3.

.按任意键继续

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

我知道这会让很多习惯于面向对象编程的人感到困惑,但如果使用得当,这并不是说这些东西本质上是邪恶的理由。有些结构是不可变的(Python的namedtuple就是这种情况),但这是另一种需要考虑的范例。

是的:结构体涉及大量内存,但它不会精确地通过执行以下操作来增加内存:

point.x = point.x + 1

相比:

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

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

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

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

他们并不邪恶。只要把它们用在正确的地方。

如果你从锤子的角度思考,你会想把螺丝当作钉子,发现螺丝更难扎进墙里,这将是螺丝的错,它们将是邪恶的。