在 Stack Overflow 上的讨论中,我已经多次看到关于可变结构体是“邪恶”的评论(例如这个问题的答案中)。
在 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的位置比克隆Eric,移动克隆并销毁原始数据更自然。这两个操作都将成功更改eric.position
的内容,但其中一个比另一个更直观。同样地,传递Eric(作为引用)以便方法可以修改他更直观。给方法传递Eric的克隆品几乎总是会令人惊讶的。任何想要改变Person
的人都必须记得请求对Person
的引用,否则他们将做错事情。eric
,那么我收到eric
或eric
的克隆品对我来说没有区别。更一般地,如果一个类型所有可观察状态都保存在以下成员中,则按值传递是安全的:
Person
是否直观取决于你要做什么。如果Person
只代表有关人员的一组数据,那么它并不会令人困惑;Person
变量确实表示抽象的值,而不是对象。(在这种情况下,重命名为PersonData
可能更合适。)如果Person
实际上是模拟一个人,即使避免了认为正在修改原始数据的陷阱,不断地创建和移动克隆品的想法也是愚蠢的。在这种情况下,仅将Person
设置为引用类型(即类)可能更自然。eric
的引用并改变他),但由于这不符合OOP的惯例,因此对于与您的代码一起工作的其他人来说仍然会很难以理解。foo
在宇宙中是指向目标唯一的引用,且没有任何东西捕获该对象的身份散列值,则突变字段 foo.X
在语义上等同于使 foo
指向一个新对象,该对象与它之前所引用的对象完全相同,但 X
包含了所需的值。对于类类型,通常很难知道是否存在多个引用指向某个东西,但是对于结构体来说很容易:它们不存在这个问题。 - supercatThing
是一个可变的类类型,那么Thing[]
将会封装对象标识——无论是否希望这样做——除非能确保数组中任何外部引用存在的Thing
都不会被改变。如果不想让数组元素封装标识,则通常必须确保它所持有的任何项都不会被改变,或者不会存在任何外部引用指向它所持有的任何项[混合方法也可以起作用]。两种方法都不是非常方便。如果Thing
是一个结构体,则Thing[]
仅封装值。 - supercatEric Lippert的例子存在几个问题。它是为了说明结构体被复制以及如果不小心使用可能会导致问题而刻意构造的。从这个例子来看,我觉得它实际上是一个不好的编程习惯的结果,而不是关于结构体或类的问题。
结构体应该只有公共成员,不需要任何封装。如果需要封装,则应该将其定义为类型/类。实际上,您不需要两种构造方式来表示同一件事情。
如果您有一个封装结构体的类,应该在类中调用方法来改变成员结构体的值。这是一个良好的编程习惯。
一个正确的实现应该如下所示。
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 表现得像预期的那样:
1 2 3 按任意键继续. . .
个人而言,当我看到以下代码时,感觉它相当笨重:
data.value.set ( data.value.get () + 1 ) ;
与简单地使用以下代码相比:
data.value++ 或 data.value = data.value + 1 ;
数据封装在传递类时很有用,可以确保以受控的方式修改值。但是,如果您只是拥有公共的set和get函数,并且这些函数仅仅是将值设置为传入的任何值,那么这如何改进仅传递公共数据结构呢?
当我在类中创建一个私有结构时,我创建该结构以将一组变量组织成一组。我希望能够在类范围内修改该结构,而不是获取该结构的副本并创建新实例。
对我来说,这会阻止结构的有效使用,用于组织公共变量,如果我需要访问控制,我会使用类。
这与结构体无关(也与C#无关),但在Java中,如果可变对象是哈希映射的键,则可能会出现问题。如果您在将其添加到映射中后更改它们并更改了其哈希码,则可能会发生一些不好的事情。
如果使用得当,我不认为它们是邪恶的。我不会在我的生产代码中使用它,但我会在类似结构化单元测试模拟的东西中使用它,在这种情况下,结构体的寿命相对较短。
以Eric的例子为例,也许您想创建该Eric的第二个实例,但进行一些调整,因为这是您测试的性质(即复制,然后修改)。如果我们只是在剩余的测试脚本中使用Eric2,那么第一个Eric发生的事情并不重要,除非您计划将其用作测试比较。
这对于测试或修改浅定义特定对象的旧代码非常有用(结构体的要点),但通过具有不可变结构体,这可以防止令人讨厌的使用。
Range<T>
类型,其成员为类型为T
的Minimum
和Maximum
字段,以及代码Range<double> myRange = foo.getRange();
,有关Minimum
和Maximum
包含什么的任何保证都应来自于foo.GetRange();
。将Range
作为公开字段结构体会清楚地表明它不会添加任何自己的行为。 - supercat
int
和bool
类型是不可变的。 - Blorgbearda[V][X] = 3.14
语法来进行原地变异。在C#中,你最好提供结构体成员变异器方法,比如'MutateV(Action<ref Vector2> mutator),并像这样使用它:
a.MutateV((v) => { v.X = 3; })(由于C#关键字
ref`的限制,示例过于简化,但通过一些解决方法应该是可能的)。 - Slipp D. Thompson