C#中的字符串不变性

我很好奇StringBuilder类是如何在内部实现的,所以我决定查看Mono的源代码并将其与Reflector的反汇编代码进行比较。 从本质上讲,Microsoft的实现使用char[]在内部存储字符串表示,并使用一堆不安全的方法来操作它。 这很简单,没有提出任何问题。 但当我发现Mono在StringBuilder中使用一个字符串时,我很困惑:

 private int _length; private string _str; 

第一个想法是:“多么无谓的StringBuilder”。 但后来我发现可以使用指针改变字符串:

 public StringBuilder Append (string value) { // ... String.CharCopy (_str, _length, value, 0, value.Length); } internal static unsafe void CharCopy (char *dest, char *src, int count) { // ... ((short*)dest) [0] = ((short*)src) [0]; dest++; src++; } 

我曾经在C / C ++中编程一点,所以我不能说这段代码让我很困惑,但我认为字符串是完全不可变的(即绝对没有办法改变它)。 所以实际的问题是:

  • 我可以创建一个完全不可变的类型吗?
  • 除性能问题外,是否有任何理由使用此类代码? (更改不可变类型的不安全代码)
  • 字符串本质上是线程安全的吗?

我可以创建一个完全不可变的类型吗?

您可以创建CLR在其上强制实现不变性的类型。 然后,您可以使用“unsafe”来关闭CLR强制机制 。 这就是为什么“不安全”被称为“不安全” – 因为它关闭了安全系统。 在不安全的代码中,如果你足够努力, 包括不可变字节和CLR中强制不变性的代码,那么进程中每个字节的内存都是可写的。

您还可以使用Reflection来打破不变性。 reflection和不安全代码都需要授予极高的信任度。

除性能问题外,是否有任何理由使用此类代码?

当然,有很多理由使用不可变数据结构。 不可变数据结构摇滚 。 使用不可变数据结构的一些好理由:

  • 不可变数据结构比可变数据结构更容易推理。 当你问“这个清单是空的吗?” 然后你会得到答案,你知道答案不仅仅是现在,而是永远。 使用可变数据结构,您实际上无法问“这个列表是空的吗?” 所有你能问的是“这个清单现在是空的吗?” 然后答案在逻辑上回答了问题“这个列表在过去的某个时刻是空的吗?”

关于不可变类型的问题的答案永远保持为真的事实具有安全隐患。 假设你有这样的代码:

 void Frob(Bar bar) { if (!IsSafe(bar)) throw something; DoSomethingDangerous(bar); } 

如果Bar是一个可变类型,那么这里就存在竞争条件; 检查后但发生危险之前,可能会在另一个线程上使条形图不安全。 如果Bar是一个不可变类型,那么问题的答案始终保持不变,这样更安全。 (想象一下,如果你可以在安全检查之后但文件打开之前改变包含路径的字符串,例如。)

  • 将不可变数据结构作为参数并将其作为结果返回并且不执行副作用的方法称为“纯方法”。 可以记忆纯方法,这可以增加内存使用以提高速度,通常可以极大地提高速度。

  • 不可变数据结构通常可以在不锁定的情况下同时在多个线程上使用。 锁定是为了防止在突变面前创建对象的不一致状态,但是不可变对象没有突变。 (一些所谓的不可变数据结构在逻辑上是不可变的,但实际上是在它们内部进行突变;想象一下例如一个查找表,它不会改变它的内容,但如果可以推断出下一个查询可能是什么,它会重新组织它的内部结构。这样的数据结构不会自动线程安全。)

  • 当从旧的结构构建新结构时,有效地重复使用其内部部件的不可变数据结构使得在不浪费大量内存的情况下“快照”程序状态变得容易。 这使得undo-redo操作无法实现。 它使编写调试工具变得更容易,可以向您展示如何进入特定的程序状态。

  • 等等。

字符串本质上是线程安全的吗?

如果每个人都遵守规则,他们就是。 如果有人使用不安全的代码或私人reflection,则不再执行规则 。 你必须相信,如果有人使用高权限代码,那么他们正在这样做,而不是改变字符串。 用你的力量只运行不安全的代码; 拥有权利的同时也被赋予了重大的责任。

那么我需要使用锁吗?

这是一个奇怪的问题。 请记住,锁是合作的 。 只有在访问特定对象的每个人都同意必须使用的锁定策略时,锁才有效。

如果用于访问特定存储位置中的特定对象的约定锁定策略是使用锁,则必须使用锁。 如果这不是商定的锁定策略那么使用锁是没有意义的; 当其他人在敞开的后门走动时,你小心地锁定和解锁前门。

如果你有一个你知道被不安全代码变异的字符串,并且你不希望看到不一致的部分突变,并且正在执行不安全突变文件的代码在该突变期间取出特定的锁,那么是,你需要在访问该字符串时使用锁。 但这种情况非常罕见; 理想情况下,没有人会使用不安全的代码来操纵另一个线程上其他代码可访问的字符串,因为这样做是一个非常糟糕的主意。 这就是为什么我们要求完全信任的代码。 这就是为什么我们要求这样一个函数的C#源代码发出一个大红旗,上面写着“这段代码不安全,请仔细检查!”

如果你不安全,也可以在C#中改变字符串(IIRC)。

没有完全不可变的类型,一个不可变的类是因为它不允许任何外部代码改变它。 使用reflection或不安全的代码,您仍然可以更改它的值。

您可以使用readonly关键字创建不可变变量,但这仅适用于值类型。 如果在引用类型上使用它,它只是受保护的引用,而不是它指向的对象。

不可变类型有几个原因,如性能和健壮性。

知道字符串是不可变的(在StringBuilder之外)意味着编译器可以根据它进行优化。 编译器永远不必生成复制字符串的代码,以防止它在作为参数传递时被更改。

从不可变类型创建的对象也可以在线程之间安全地传递。 由于它们无法更改,因此不同的线程不会同时更改它们,因此无需同步访问它们。

不可变类型可用于避免编码错误。 如果您知道不应更改某个值,通常最好确保不会错误地更改它。

这里没有黑魔法。 字符串类是不可变的,因为它没有任何允许您修改内部字符串的公共字段,属性或方法。 任何改变字符串的方法都会返回一个新的字符串实例。 你当然也可以用你自己的课程来做这件事。

您可以阅读这些post不可变类型:了解它们的好处并使用它们

并且在没有同步痛苦的multithreading环境中管理状态

此外, NDepend工具还带有一些工具来处理不可变类型和纯方法 。

我可以创建一个完全不可变的类型吗?

是。 有一个构造函数来设置私有字段,只获取属性,没有方法。

除性能问题外,是否有任何理由使用此类代码?

一个例子:这样的类型不需要从多个并发线程安全地使用锁,这使得正确的代码更容易编写(没有锁定出错)。

附加:足够特权的代码总是可以绕过.NET保护:要么reflection读取和写入私有字段,要么是不安全的代码来直接操作对象的内存。

这在.NET之外是正确的,一个特权进程(即具有一个“上帝”特权的进程或线程令牌,例如启用Take Ownership)可以进入任何其他进程加载dll,注入运行任意代码的线程,读取或写内存(包括覆盖执行预防等)。 系统的完整性与系统所有者的合作一样强大。