为什么不';t字符串.子字符串与源字符串共享内存

本文关键字:字符串 共享 内存 为什么不 | 更新日期: 2023-09-27 17:58:42

众所周知,字符串在中。NET是不可变的。(好吧,不是100%完全不可变,而是设计不可变的,并且被任何合理的人使用。)

这使得它基本上可以,例如,以下代码只在两个变量中存储对同一字符串的引用:

string x = "shark";
string y = x.Substring(0);
// Proof:
fixed (char* c = y)
{
    c[4] = 'p';
}
Console.WriteLine(x);
Console.WriteLine(y);

上述输出:

sharp
sharp

显然,xy指的是相同的string对象。所以我的问题是:为什么Substring不总是与源字符串共享状态字符串本质上是一个有长度的char*指针,对吧?因此,在我看来,至少在理论上,应该允许以下内容分配一个单个内存块来容纳5个字符,其中两个变量只是指向该(不可变)块中的不同位置:

string x = "shark";
string y = x.Substring(1);
// Does c[0] point to the same location as x[1]?
fixed (char* c = y)
{
    c[0] = 'p';
}
// Apparently not...
Console.WriteLine(x);
Console.WriteLine(y);

上述输出:

shark
park

为什么不';t字符串.子字符串与源字符串共享内存

原因有二:

  • 字符串元数据(例如长度)与字符存储在同一个内存块中,允许一个字符串使用另一个字符串的部分字符数据意味着您必须为大多数字符串分配两个内存块,而不是一个。由于大多数字符串不是其他字符串的子字符串,因此额外的内存分配将比重用部分字符串所能获得的内存消耗更多。

  • 在字符串的最后一个字符之后存储了一个额外的NUL字符,以使该字符串也可用于期望以null结尾的字符串的系统函数。不能将额外的NUL字符放在作为另一个字符串一部分的子字符串之后。

我认为C#字符串是以null结尾的——虽然这是一个不应该涉及托管使用者的实现细节,但在某些情况下(例如封送处理)它很重要。

此外,如果子字符串与长得多的字符串共享缓冲区,这意味着对短子字符串的引用将阻止长字符串被收集。以及引用相同缓冲区的字符串引用的老鼠窝的可能性。

添加到其他答案:

显然,Java标准类是这样做的:String.substring()返回的字符串重用原始字符串的内部字符数组(源代码,或者查看Sun的JDK源代码)。

问题是,这意味着在所有子字符串都符合GC条件之前(因为它们共享支持字符数组),不能对原始字符串进行GC。如果你从一个大字符串开始,从中提取一些较小的字符串,然后丢弃大字符串,这可能会导致内存浪费。例如,在解析输入文件时,这很常见。

当然,一个聪明的GC可能会在值得的时候通过复制字符数组来解决这个问题(我不知道Sun JVM可能会这样做),但增加的复杂性可能是根本不实现这种共享行为的原因。

有很多方法可以实现类似String的东西:

  1. 让一个"String"对象有效地包含一个数组,这意味着数组中的所有字符都在字符串中。这就是.net的实际作用。
  2. 让每个"String"都是一个类,它包含一个数组引用以及一个起始偏移量和长度。问题:创建大多数字符串需要实例化两个对象,而不是一个。
  3. 让每个"字符串"都是一个包含数组引用以及起始偏移和长度的结构。问题:对字符串类型字段的赋值将不再是原子的。
  4. 有两种或两种以上类型的"字符串"对象——那些包含数组中的所有字符的对象,以及那些包含对另一个字符串的引用以及偏移量和长度的对象。问题:这将要求字符串的许多方法都是虚拟的。
  5. 让每个"字符串"都是一个特殊的类,它包括一个起始偏移量和长度,一个对可能是或可能不是同一对象的对象引用,以及一个内置的字符数组。在字符串包含自己的字符(因为所有字符)的常见情况下,这会浪费一点空间,但会允许相同的代码处理包含自己字符的字符串或从其他字符串"借用"的字符串。
  6. 具有通用的ImmutableArray<T>类型(将继承ReadableArray<T>),并具有ImmutableArray<字符>可与字符串互换。不可变数组有很多用途;字符串可能是最常见的用例,但不是唯一的用例。
  7. 具有通用的ImmutableArray类型<T>类型,而且还有一个ImmutableArraySegment<T>类,都继承自ImmutableArrayBase<T>。这将需要许多方法是虚拟的,这可能是我最喜欢的可能性。

请注意,这些方法中的大多数在至少某些使用场景中都有显著的局限性。

我认为这些是CLR优化,与程序员无关,因为你不应该做你正在做的事情。您应该假设它每次都是一个新字符串(作为程序员)。

在使用反射器查看Substring方法后,我发现如果在Substring方法中传递0,它将返回相同的对象。

[SecurityCritical]
private unsafe string InternalSubString(int startIndex, int length, bool fAlwaysCopy)
{
    if (((startIndex == 0) && (length == this.Length)) && !fAlwaysCopy)
    {
        return this;
    }
    string str = FastAllocateString(length);
    fixed (char* chRef = &str.m_firstChar)
    {
        fixed (char* chRef2 = &this.m_firstChar)
        {
            wstrcpy(chRef, chRef2 + startIndex, length);
        }
    }
    return str;
}

这会给intern表增加复杂性(或者至少增加更多智能)。想象一下,您已经在实习生表中有两个条目"待定"answers"弯曲",代码如下:

var x = "pending";
var y = x.Substring(1);

实习生表中的哪个条目会被认为是热门项目?