提问人:Dirk Vollmar 提问时间:1/14/2009 最后编辑:CommunityDirk Vollmar 更新时间:5/2/2020 访问量:104956
为什么可变结构是“邪恶的”?
Why are mutable structs “evil”?
答:
从哪里开始;-p
埃里克·利珀特(Eric Lippert)的博客总是适合引用:
这是可变的另一个原因 值类型是邪恶的。尝试始终 使值类型不可变。
首先,你往往很容易失去变化......例如,从列表中获取内容:
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;
不太关键的是,存在尺寸问题;可变对象往往具有多个属性;然而,如果你有一个包含两个 s、a、a 和 a 的结构,你可以非常快速地烧毁大量内存。对于一个类,多个调用方可以共享对同一实例的引用(引用很小)。int
string
DateTime
bool
评论
++
SomeProperty
SomeProperty
struct
它与结构没有任何关系(也与 C# 无关),但在 Java 中,当可变对象是哈希映射中的键时,您可能会遇到问题。如果您在将它们添加到地图后更改它们并且它更改了其哈希代码,则可能会发生邪恶的事情。
评论
我不会说邪恶,但可变性通常是程序员过度渴望提供最大功能的标志。实际上,这通常不是必需的,这反过来又使界面更小,更易于使用,更难使用错误(=更健壮)。
例如,竞争条件下的读/写和写/写冲突。这些根本不可能发生在不可变的结构中,因为写入不是有效的操作。
另外,我声称几乎实际上从来不需要可变性,程序员只是认为它可能在未来。例如,更改日期根本没有意义。相反,基于旧日期创建一个新日期。这是一种廉价的操作,因此性能不是考虑因素。
评论
float
结构是值类型,这意味着它们在传递时会被复制。
因此,如果您更改副本,则只会更改该副本,而不是原始副本,也不会更改可能存在的任何其他副本。
如果你的结构是不可变的,那么所有按值传递的自动副本都将是相同的。
如果你想改变它,你必须有意识地通过使用修改后的数据创建一个结构的新实例来实现。(不是副本)
评论
可变数据有许多优点和缺点。百万美元的缺点是混叠。如果同一个值在多个地方被使用,并且其中一个地方改变了它,那么它似乎已经神奇地改变了使用它的其他地方。这与竞争条件相关,但并不完全相同。
有时,百万美元的优势是模块化。可变状态可以让你从不需要知道的代码中隐藏不断变化的信息。
《诠释者的艺术》详细地探讨了这些权衡,并举了一些例子。
评论
值类型基本上表示不可变的概念。Fx,拥有整数、向量等数学值然后能够修改它是没有意义的。这就像重新定义一个值的含义。与其更改值类型,不如分配另一个唯一值。考虑通过比较其属性的所有值来比较值类型的事实。关键是,如果属性相同,则它是该值的相同通用表示形式。
正如 Konrad 所提到的,更改日期也没有意义,因为该值表示该唯一时间点,而不是具有任何状态或上下文依赖关系的时间对象的实例。
希望这对您有意义。可以肯定的是,它更多的是关于您尝试使用值类型捕获的概念,而不是实际细节。
评论
int
假设您有一个包含 1,000,000 个结构的数组。每个结构体都代表一个权益,其中包含 bid_price、offer_price(可能是小数)等内容,这是由 C#/VB 创建的。
想象一下,数组是在非托管堆中分配的内存块中创建的,以便其他一些本机代码线程能够同时访问该数组(可能是一些执行数学运算的高性能代码)。
想象一下,C#/VB 代码正在侦听价格变化的市场馈送,该代码可能必须访问数组的某些元素(无论哪种证券),然后修改某些价格字段。
想象一下,这是每秒数万甚至数十万次。
好吧,让我们面对事实,在这种情况下,我们确实希望这些结构是可变的,它们需要是可变的,因为它们被其他一些本机代码共享,所以创建副本无济于事;它们之所以需要,是因为以这样的速率复制大约 120 字节的结构是疯狂的,尤其是当更新实际上可能只影响一两个字节时。
雨 果
评论
就我个人而言,当我查看代码时,以下内容对我来说看起来很笨拙:
data.value.set ( data.value.get () + 1 ) ;
而不是简单地
数据值++ ;或 data.value = data.value + 1 ;
在传递类时,数据封装非常有用,并且您希望确保以受控方式修改值。但是,当您拥有公共集合并获取函数时,这些函数的作用只不过是将值设置为传入的内容,这如何比简单地传递公共数据结构有所改进?
当我在类中创建私有结构时,我创建了该结构以将一组变量组织到一个组中。我希望能够在类范围内修改该结构,而不是获取该结构的副本并创建新实例。
对我来说,这阻止了用于组织公共变量的结构的有效使用,如果我想要访问控制,我会使用一个类。
评论
从程序员的角度来看,还有其他一些极端情况可能会导致不可预测的行为。
不可变值类型和只读字段
// 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);
所以,只要你和团队的其他成员清楚地了解你在做什么,可变结构就不是邪恶的。但是,在很多极端情况下,程序行为与预期不同,这可能会导致微妙的难以产生和难以理解的错误。
评论
public static void IncrementI(ref Mutable m) { m.I++; }
具有公共可变字段或属性的结构体并不邪恶。
改变“this”的结构方法(与属性设置器不同)有些邪恶,只是因为 .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);
对于每种方法,请回答以下问题:
- 假设该方法不使用任何“不安全”代码,它可能会修改 foo 吗?
- 如果在调用方法之前不存在对“foo”的外部引用,那么调用方法之后是否存在外部引用?
答案:
问题 1: : 否 (明确意图) : 是 (明确意图) : 是 (不确定意图) 问题 2: : 否 : 否
(除非不安全)
: 是Method1()
Method2()
Method3()
Method1()
Method2()
Method3()
Method1 无法修改 foo,并且永远不会获得引用。Method2 获取对 foo 的短暂引用,它可以使用它以任何顺序修改 foo 的字段任意次数,直到它返回,但它不能保留该引用。在 Method2 返回之前,除非它使用不安全的代码,否则可能由其“foo”引用制作的任何和所有副本都将消失。与 Method2 不同,Method3 获得了对 foo 的可混杂共享引用,并且不知道它会用它做什么。它可能根本不会改变 foo,它可能会改变 foo 然后返回,或者它可能会将 foo 引用到另一个线程,该线程可能会在某个任意的未来时间以某种任意方式改变它。限制 Method3 对传递给它的可变类对象执行的操作的唯一方法是将可变对象封装到只读包装器中,这既丑陋又繁琐。
结构数组提供了美妙的语义。给定 Rectangle 类型的 RectArray[500],例如,如何将元素 123 复制到元素 456,然后在一段时间后将元素 123 的宽度设置为 555,而不干扰元素 456,这是很清楚的。“RectArray[432] = RectArray[321];...;矩形数组[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] = 新建 RectClass(RectClassArray[321]);RectClassArray[321]。X = 555;”要知道应该做什么,就必须更多地了解 RectClass(例如,它是否支持复制构造函数、复制自方法等)和数组的预期用法。远不如使用结构体干净。
可以肯定的是,不幸的是,除了数组之外,任何容器类都没有很好的方法来提供结构数组的干净语义。如果想要用字符串等索引集合,最好的办法可能是提供一个通用的“ActOnItem”方法,该方法将接受索引的字符串、泛型参数和委托,该委托将通过引用传递泛型参数和集合项。这将允许与结构数组几乎相同的语义,但除非可以说服 vb.net 和 C# 人员提供一个很好的语法,否则即使代码具有合理的性能,代码看起来也会很笨拙(传递泛型参数将允许使用静态委托,并避免创建任何临时类实例的需要)。
就我个人而言,我对 Eric Lippert 等人对可变值类型的仇恨感到恼火。它们提供的语义比到处使用的混杂引用类型要干净得多。尽管 .net 对值类型的支持存在一些限制,但在许多情况下,可变值类型比任何其他类型的实体都更适合。
评论
Rectangle
Rectangle
Bounds
form.Bounds.X = 10;
可变结构不是邪恶的。
它们在高性能情况下是绝对必要的。例如,当缓存行和/或垃圾回收成为瓶颈时。
我不会称在这些完全有效的用例中使用不可变结构体是“邪恶的”。
我可以看到 C# 的语法无助于区分值类型或引用类型成员的访问,因此我完全赞成使用强制不可变性的不可变结构,而不是可变结构。
然而,与其简单地将不可变的结构贴上“邪恶”的标签,我建议接受这种语言并倡导更有用和建设性的经验法则。
例如:“structs 是值类型,默认情况下会复制。如果你不想复制它们,你需要一个引用“或”先尝试使用只读结构”。
评论
struct
当某物可以变异时,它就会获得一种认同感。
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));
因为是可变的,所以考虑改变埃里克的位置比克隆埃里克、移动克隆和破坏原件更自然。这两种操作都可以成功地更改 的内容,但一个操作比另一个操作更直观。同样,将 Eric 传递(作为参考)以获取修改他的方法也更直观。给一个方法一个Eric的克隆几乎总是令人惊讶的。任何想要变异的人都必须记得要求参考,否则他们会做错事。Person
eric.position
Person
Person
如果使类型不可变,问题就会消失;如果我不能修改,那么我收到还是克隆对我来说没有区别。更一般地说,如果某个类型的所有可观察状态都保存在以下成员中,则按值传递类型是安全的:eric
eric
eric
- 变
- 引用类型
- 按值安全传递
如果满足这些条件,则可变值类型的行为类似于引用类型,因为浅拷贝仍允许接收方修改原始数据。
不过,不可变的直观性取决于你要做什么。如果只是代表一组关于一个人的数据,那么它没有什么不直观的; 变量真正表示抽象值,而不是对象。(在这种情况下,将其重命名为 .如果真的在对一个人本身进行建模,那么不断创建和移动克隆的想法是愚蠢的,即使你已经避免了认为你正在修改原始人的陷阱。在这种情况下,简单地创建引用类型(即类)可能更自然。Person
Person
Person
PersonData
Person
Person
诚然,正如函数式编程告诉我们的那样,使一切都不可变是有好处的(没有人可以偷偷地保留对他的引用并改变他),但由于这在 OOP 中不是惯用的,所以对于使用你的代码的其他人来说,它仍然会是不直观的。eric
评论
foo
foo.X
foo
X
Thing
Thing[]
Thing
Thing
Thing[]
如果您曾经使用过 C/C++ 等语言进行编程,那么结构体可以用作可变结构。只要用 ref 传递它们,就没有什么可以出错的。我发现的唯一问题是 C# 编译器的限制,在某些情况下,我无法强制愚蠢的东西使用对结构的引用,而不是 Copy(例如当结构是 C# 类的一部分时)。
所以,可变结构不是邪恶的,C#使它们变得邪恶。我一直在 C++ 中使用可变结构,它们非常方便和直观。相比之下,C# 使我完全放弃了结构作为类成员,因为它们处理对象的方式。他们的便利使我们付出了代价。
评论
readonly
int[]
如果使用得当,我不相信它们是邪恶的。我不会把它放在我的生产代码中,但我会用于结构化单元测试模拟之类的东西,其中结构的生命周期相对较短。
以 Eric 为例,也许您想创建该 Eric 的第二个实例,但要进行调整,因为这是测试的性质(即复制,然后修改)。如果我们只是在测试脚本的其余部分使用 Eric2,那么 Eric 的第一个实例会发生什么并不重要,除非您打算将他用作测试比较。
这对于测试或修改浅层定义特定对象(结构点)的遗留代码非常有用,但是通过具有不可变的结构,可以防止其烦人的使用。
评论
Range<T>
Minimum
Maximum
T
Range<double> myRange = foo.getRange();
Minimum
Maximum
foo.GetRange();
Range
埃里克·利珀特先生的例子有几个问题。它是为了说明结构被复制的观点,以及如果你不小心,这可能是一个问题。看看这个例子,我认为这是不良编程习惯的结果,而不是结构或类的真正问题。
结构应该只有公共成员,不需要任何封装。如果是这样,那么它确实应该是一个类型/类。你真的不需要两个结构来表达同样的事情。
如果类包含结构,则可以调用类中的方法来改变成员结构。这就是我作为一个良好的编程习惯会做的事情。
正确的实现方式如下。
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());
}
看起来这是编程习惯的问题,而不是结构本身的问题。结构应该是可变的,这就是想法和意图。
瞧,更改的结果按预期运行:
1 2 3 按任意键继续。 。 。
评论
如果你坚持结构的用途(在 C#、Visual Basic 6、Pascal/Delphi、C++ 结构类型(或类)中,当它们不用作指针时),你会发现结构只不过是一个复合变量。这意味着:您将把它们视为一组打包的变量,使用一个公用名(您从中引用成员的记录变量)。
我知道这会让很多深深习惯了 OOP 的人感到困惑,但这还不足以说明如果使用得当,这些东西本质上是邪恶的。有些结构是不可变的(Python 就是这种情况),但这是另一种需要考虑的范式。namedtuple
是的:结构体涉及大量内存,但执行以下操作不会恰恰是增加内存:
point.x = point.x + 1
与以下产品相比:
point = Point(point.x + 1, point.y)
内存消耗至少相同,甚至在不可变情况下甚至更多(尽管对于当前堆栈,这种情况是暂时的,具体取决于语言)。
但是,最后,结构是结构,而不是对象。在 POO 中,对象的主要属性是它们的身份,大多数时候它不超过它的内存地址。Struct 代表数据结构(不是一个合适的对象,因此它们无论如何都没有身份),并且可以修改数据。在其他语言中,record(而不是 struct,就像 Pascal 的情况一样)是这个词,并且具有相同的目的:只是一个数据记录变量,旨在从文件中读取、修改和转储到文件中(这是主要用途,在许多语言中,您甚至可以在记录中定义数据对齐方式,而正确称为 Objects 不一定是这种情况)。
想要一个好榜样吗?结构用于轻松读取文件。Python 之所以有这个库,是因为它是面向对象的,不支持结构,所以它必须以另一种方式实现它,这有点丑陋。实现结构的语言具有该特性...内置。尝试使用 Pascal 或 C 等语言的适当结构读取位图标头。这将很容易(如果结构体被正确构建和对齐;在Pascal中,你不会使用基于记录的访问,而是使用函数来读取任意二进制数据)。因此,对于文件和直接(本地)内存访问,结构比对象更好。至于今天,我们已经习惯了 JSON 和 XML,因此我们忘记了二进制文件的使用(以及副作用,使用结构)。但是,是的:它们存在,并且有目的。
他们不是邪恶的。只需将它们用于正确的目的。
如果你从锤子的角度来思考,你会想把螺丝当钉子,发现螺丝更难墙里,这将是螺丝的错,它们将是邪恶的。
评论
int
bool
int
bool
ref
关键字的限制,示例过于简化,但应该可以使用一些解决方法)。.
a[V][X] = 3.14
a.MutateV((v) => { v.X = 3; })
x
ldloc.0
T
ref T
<>