代码之家  ›  专栏  ›  技术社区  ›  n535

C中的字符串不变性#

  •  25
  • n535  · 技术社区  · 15 年前

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

    private int _length;
    private string _str;
    

    第一个想法是:“多么愚蠢的架线工。”但后来我发现使用指针改变字符串是可能的:

    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++编程,所以我不能说这个代码让我感到困惑,但我认为字符串是完全不可变的(即绝对没有办法改变它)。所以实际问题是:

    • 我可以创建一个完全不变的类型吗?
    • 除了性能问题,是否有任何理由使用这种代码? (更改不可变类型的不安全代码)
    • 那么字符串本身是否是线程安全的?
    6 回复  |  直到 15 年前
        1
  •  43
  •   Eric Lippert    15 年前

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

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

    您还可以使用反射来打破不变性。反射代码和不安全代码都要求授予极高的信任级别。

    除了性能问题,是否有任何理由使用这种代码?

    当然,使用不可变数据结构有很多原因。不可变的数据结构 岩石 . 使用不可变数据结构的一些好理由:

    • 不可变数据结构比可变数据结构更容易解释。当你问“这个列表是空的吗?”你得到一个答案,然后你知道答案不仅是正确的,而且是永远正确的。对于可变数据结构,您实际上不能问“这个列表是空的吗?”你能问的就是“这张单子现在是空的吗?”然后,答案逻辑地回答了这个问题:“这个列表在过去的某个时刻是空的吗?”

    对于一个关于不可变类型的问题的答案永远保持不变的事实具有安全性的含义。假设您有这样的代码:

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

    如果BAR是可变类型,则此处存在争用条件;BAR在另一线程上可能不安全。 之后 支票但 之前 危险的事情发生了。如果bar是不变的类型,那么问题的答案在整个过程中保持不变,这样更安全。(想象一下,如果可以改变包含路径的字符串 之后 安全检查但是 之前 例如,文件已打开。)

    • 将不可变的数据结构作为参数,并返回它们作为结果,并且不执行任何副作用的方法称为“纯方法”。纯方法可以被记忆化,用增加的内存使用来提高速度,通常速度会大大提高。

    • 不可变数据结构通常可以同时在多个线程上使用,而无需锁定。锁定是为了防止对象在面对突变时产生不一致的状态,但不可变的对象没有突变。(有些所谓的不可变数据结构在逻辑上是不可变的,但实际上它们本身也会发生变化;例如,假设一个查找表不会改变其内容,但如果它能够推断出下一个查询可能是什么,则会重新组织其内部结构。这样的数据结构不会自动被线程安全保护。)

    • 不可变的数据结构,当从旧结构构建新结构时,可以有效地重新使用其内部部分,这使得在不浪费大量内存的情况下,很容易“快照”程序的状态。这使得撤消重做操作的实现变得很简单。它使编写调试工具变得更容易,这些工具可以向您展示如何到达特定的程序状态。

    • 等等。

    那么字符串本身是否是线程安全的?

    如果每个人都按规则行事,他们就是。如果有人使用不安全的代码或私有反射,那么 不再有规则执行 . 您必须相信,如果有人正在使用高特权代码,那么他们这样做是正确的,并且不会改变字符串。用你的能力运行不安全的代码只是为了好;有了强大的能力就有了巨大的责任。

    所以我是否需要使用锁?

    这是个奇怪的问题。记住,锁是 合作的 . 只有当 每个人 访问特定对象与必须使用的锁定策略一致。

    如果 商定 访问特定存储位置中特定对象的锁定策略是使用锁。如果这不是商定的锁定策略,那么使用锁是毫无意义的;当其他人在打开的后门中行走时,你小心地锁定和解锁前门。

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

        2
  •  3
  •   leppie    15 年前

    如果你变得不安全,也有可能改变C(IIRC)中的字符串。

        3
  •  3
  •   Robert Harvey    15 年前

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

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

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

    已知字符串是不可变的事实(在 StringBuilder )这意味着编译器可以基于此进行优化。编译器从不需要生成代码来复制字符串,以防止它作为参数传递时被更改。

    从不可变类型创建的对象也可以在线程之间安全地传递。因为它们不能被更改,所以不同线程在同一时间更改它们是没有风险的,所以不需要同步访问它们。

    不可变类型可以用来避免编码错误。如果你知道一个值不应该被改变,那么通常最好确保它不会被错误地改变。

        4
  •  2
  •   Hans Passant    15 年前

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

        6
  •  1
  •   Richard    15 年前

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

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

    除了性能问题,是否有任何理由使用这种代码?

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

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

    这在.NET之外是正确的,特权进程(即具有进程或具有“上帝”特权的线程令牌,例如启用所有权)可以闯入任何其他进程加载DLL、注入运行任意代码的线程、读或写内存(包括覆盖执行保护等)。系统的完整性只有在系统所有者的合作下才能得到加强。