为什么这个结构体是可变的呢?什么时候可以接受可变结构?
本文关键字:什么时候 结构 结构体 为什么 | 更新日期: 2023-09-27 18:14:15
Eric Lippert告诉我应该"尽量让值类型永远不可变",所以我想我应该尽量让值类型永远不可变。
但是,我刚刚在System.Web
程序集中发现了这个内部可变结构体System.Web.Util.SimpleBitVector32
,这使我认为必须有一个很好的理由使用可变结构体。我猜他们这么做的原因是因为它在测试中表现得更好,他们把它保存在内部以防止误用。然而,这只是猜测。
我已经把这个结构体的源代码加了p。是什么证明了使用可变结构的设计决策?一般来说,这种方法可以获得什么样的好处,什么时候这些好处足以证明潜在的损害是合理的?
[Serializable, StructLayout(LayoutKind.Sequential)]
internal struct SimpleBitVector32
{
private int data;
internal SimpleBitVector32(int data)
{
this.data = data;
}
internal int IntegerValue
{
get { return this.data; }
set { this.data = value; }
}
internal bool this[int bit]
{
get {
return ((this.data & bit) == bit);
}
set {
int data = this.data;
if (value) this.data = data | bit;
else this.data = data & ~bit;
}
}
internal int this[int mask, int offset]
{
get { return ((this.data & mask) >> offset); }
set { this.data = (this.data & ~mask) | (value << offset); }
}
internal void Set(int bit)
{
this.data |= bit;
}
internal void Clear(int bit)
{
this.data &= ~bit;
}
}
考虑到有效负载是一个32位整数,我想说这可以很容易地写成一个不可变结构体,可能对性能没有影响。无论你是调用mutator方法来改变一个32位字段的值,还是用一个新的32位结构体替换一个32位结构体,你仍然在做完全相同的内存操作。
可能有人想要一些像数组一样的东西(而实际上只是32位整数中的位),所以他们决定使用索引器语法,而不是不太明显的。withthesebitschanged()方法,返回一个新的结构体。由于它不会被微软网络团队以外的任何人直接使用,甚至可能也不会被网络团队中的很多人使用,我想他们在设计决策方面比构建公共api的人有更多的回旋余地。
所以,不,可能不是这样的性能——这可能只是一些程序员在编码风格上的个人偏好,从来没有任何令人信服的理由去改变它。
如果你在寻找设计指南,我不会花太多时间去看那些没有为公众消费而优化的代码。
实际上,如果你在。net框架中搜索包含BitVector的所有类,你会发现一堆这样的野兽:-)
- System.Collections.Specialized。BitVector32(唯一的公共的…)
- System.Web.Util。SafeBitVector32(线程安全)
- System.Web.Util.SimpleBitVector32
- System.Runtime.Caching。SafeBitVector32(线程安全)
- System.Configuration。SafeBitVector32(线程安全)
- System.Configuration.SimpleBitVector32
如果你看这里是驻留的SSCLI(微软共享源CLI,又名转子)源的System.Configuration。SimpleBitVector32,你会发现这个注释:
//
// This is a cut down copy of System.Collections.Specialized.BitVector32. The
// reason this is here is because it is used rather intensively by Control and
// WebControl. As a result, being able to inline this operations results in a
// measurable performance gain, at the expense of some maintainability.
//
[Serializable()]
internal struct SimpleBitVector32
我相信这说明了一切。我认为System.Web.Util更详细,但建立在相同的基础上。
SimpleBitVector32
是可变的,我怀疑,出于与BitVector32
可变的相同原因。在我看来,不可变的准则只是一个准则;但是,应该有一个真正好的理由这样做。
还要考虑Dictionary<TKey, TValue>
-我在这里讨论一些扩展的细节。字典的Entry
结构体是可变的——你可以随时改变TValue。但是,Entry
在逻辑上表示值。
可变性必须有意义。我同意@JoeWhite: 有人想要一些像数组一样的东西(而实际上只是32位整数中的位);而且,这两个BitVector结构体可以很容易地…不可变的。
但是,作为一个笼统的声明,我不同意,这可能只是一些程序员在编码风格上的个人偏好,更倾向于,从来没有[也没有]任何令人信服的理由来改变它。只需知道并理解使用可变结构的责任。编辑
声明一下,我确实衷心地同意应该总是尝试使结构不可变。如果您发现需求决定了成员的可变性,请重新审视设计决策并让同伴参与进来。
在考虑可变值类型和不可变类型时,我最初对自己的性能评估并不自信。然而,正如@David指出的那样,Eric Lippert写道:
有时候你需要把性能的每一点都写下来在一个系统之外。在这种情况下,你有时不得不做一个在代码干净、纯粹、健壮之间进行权衡,可理解的,可预测的,可修改的和代码是没有的
我保存pure,因为可变结构体不符合结构体应该不可变的纯粹理想。编写可变结构体有副作用:不稳定性和可预测性会受到损害,正如Eric继续解释的那样:
Eric的观点是,作为设计师和/或开发人员,你需要做出有意识的和知情的决定。你是如何获得信息的?Eric也解释说:可变值类型…的行为以一种很多人觉得有违直觉的方式,因此使编写有bug的代码(或容易纠正的代码)变得容易不小心变成了有bug的代码。)但是,它们确实很快。
我会考虑编写两个基准测试解决方案——一个使用可变结构,一个使用不可变结构,然后运行一些切合实际的以用户场景为中心的基准测试。但问题是:不要选快的那个。相反,应该在运行基准测试之前决定慢到不能接受.
我们知道修改值类型比创建一个新的值类型要快;但是考虑到的正确性:
如果两种解决方案都可以接受,则选择清洁的方案;正确且足够快。
关键是快速足够以抵消选择可变而非不可变的副作用。
如下所示,对32位或64位向量使用struct是合理的,但有一些注意事项:
- 我建议使用Interlocked。当对结构执行任何更新时,CompareExchange循环,而不是直接使用普通的布尔运算符。如果一个线程试图写第3位,而另一个线程试图写第8位,那么除了稍微延迟一点之外,这两个操作都不应该干扰另一个线程。联锁的使用。CompareExchange循环将避免错误行为的可能性(线程1读取值,线程2读取旧值,线程1写入新值,线程2写入基于旧值计算的值并取消线程1的更改),而不需要任何其他类型的锁定。
- 结构成员,除了属性设置器,应该避免修改"this"。最好使用静态方法,它接受结构作为引用参数。从语义和性能的角度来看,调用修改"this"的结构成员通常与调用接受该成员作为引用参数的静态方法相同,但有一个关键区别:如果试图通过引用将只读结构传递给静态方法,则会得到编译器错误。相比之下,如果在只读结构中调用修改"this"的方法,则不会出现任何编译器错误,但不会发生预期的修改。由于即使是可变结构在某些上下文中也可以被视为只读结构,因此在发生这种情况时获得编译器错误要比拥有可以编译但无法工作的代码要好得多。
如果我理解正确的话,您不能仅仅通过使用SerializableAttribute
来创建可序列化的不可变结构体。这是因为在反序列化期间,序列化程序实例化结构的默认实例,然后设置实例化后的所有字段。如果它们是只读的,反序列化将失败。
因此,struct 必须是可变的,否则就需要一个复杂的序列化系统。