我能创建一个完全不可变的类型吗?
你可以创建一个CLR强制执行不可变性的类型,然后使用"unsafe"来关闭CLR执行机制。这就是为什么"unsafe"被称为"不安全"的原因,因为它关闭了安全系统。在不安全的代码中,如果你足够努力,就可以将进程中的每个字节的内存都设为可写状态,包括不可变字节和CLR中执行不可变性的代码。
你也可以使用反射来打破不可变性。反射和不安全的代码都需要授予极高的信任级别。
除了性能问题之外,还有什么理由使用这样的代码吗?
当然,使用不可变数据结构有很多好处。不可变数据结构非常好。以下是一些使用不可变数据结构的好理由:
- 与可变数据结构相比,不可变数据结构更容易推理。当你询问“这个列表是否为空?”并获得答案时,你知道答案不仅在现在正确,而且永远都是正确的。对于可变数据结构,你实际上无法询问“这个列表是否为空?”你只能问“这个列表现在是否为空?”,然后答案逻辑上回答了问题“这个列表过去是否为空?”
关于不可变类型的问题的答案保持真实永久性,这具有安全性的影响。假设你有以下代码:
void Frob(Bar bar)
{
if (!IsSafe(bar)) throw something;
DoSomethingDangerous(bar);
}
如果Bar是可变类型,则存在竞争条件;在检查之后但在发生危险情况之前,bar可能会在另一个线程上变得不安全。如果Bar是不可变类型,则问题的答案始终保持不变,这样更安全。 (例如,想象一下在安全检查之后但在打开文件之前可以更改包含路径的字符串的情况。)
将不可变数据结构作为参数并将其作为结果返回且不执行副作用的方法称为“纯方法”。纯方法可以进行记忆化,这会以增加的内存使用量换取增加的速度,通常是大幅增加的速度。
不可变数据结构通常可以在多个线程上同时使用而无需锁定。锁定用于防止在面对突变时对象的不一致状态的创建,但是不可变对象没有突变。(某些所谓的不可变数据结构在逻辑上是不可变的,但实际上在其内部进行突变;例如,想象一下查找表,它不会更改其内容,但会重新组织其内部结构,如果可以推断出下一个查询可能是什么。这样的数据结构不会自动线程安全。)
在有效地重复使用其内部部件的不可变数据结构中,构建新结构以从旧结构中"拍摄快照"会使程序状态变得更容易,而不会浪费大量内存。这使得撤销-重做操作变得微不足道。编写调试工具以显示如何达到特定程序状态也变得更加容易。
如果所有人都遵守规则,则字符串是线程安全的。如果有人使用不安全代码或私有反射,则不存在规则执行。您必须信任,如果有人使用高特权代码,则他们正在正确地使用它,并且不会对字符串进行突变。仅出于善意使用您的运行不安全代码的权力;伴随着强大的权力而来的是巨大的责任。
那么我是否需要使用锁定?这是一个奇怪的问题。请记住,锁是协作性的。只有当访问特定对象的所有人都同意必须使用锁定策略时,锁才起作用。如果这不是约定俗成的锁定策略,则使用锁是无意义的;您小心地锁定和解锁前门,而其他人则走在打开的后门中。
如果您知道由不安全代码进行突变的字符串,并且不希望看到不一致的部分突变,并且执行不安全突变的代码记录了在该突变期间取出特定锁,则是的,在访问该字符串时需要使用锁定。但是,这种情况非常罕见;理想情况下,不会有人使用不安全代码来操作另一个线程上的其他代码可以访问的字符串,因为这样做是一个非常糟糕的主意。这就是为什么我们要求对执行此操作的代码进行完全信任。这就是为什么我们要求用于此类函数的C#源代码挥舞着一个大红旗,上面写着“此代码是不安全的,请认真审查!”