.net 为什么string.Substring不与源字符串共享内存?

zsohkypk  于 2023-11-20  发布在  .NET
关注(0)|答案(7)|浏览(100)

我们都知道,.NET中的字符串是不可变的(好吧,not 100% totally immutable,但无论如何,它在设计上是不可变的,并且被任何合理的人用作不可变的字符串)。
这使得它基本上是确定的,例如,下面的代码只是在两个变量中存储对同一字符串的引用:

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

yvfmudvl

yvfmudvl1#

原因有二:

  • 字符串Meta数据(例如长度)与字符存储在同一个内存块中,如果一个字符串使用另一个字符串的部分字符数据,那么你需要为大多数字符串分配两个内存块,而不是一个。由于大多数字符串不是其他字符串的子串,那么额外的内存分配将比重用部分字符串所获得的内存消耗更多。
  • 在字符串的最后一个字符之后存储了一个额外的NUL字符,以使该字符串也可用于期望以null结尾的字符串的系统函数。您不能将该额外的NUL字符放在属于另一个字符串的子字符串之后。
nsc4cvqm

nsc4cvqm2#

C#“字符串都是空终止的,长度前缀”-虽然这是一个实现细节,不应该涉及托管消费者,但在某些情况下(例如封送)它很重要。
另外,如果一个子串与一个长得多的字符串共享一个缓冲区,这意味着对短子串的引用将阻止收集长字符串。并且有可能出现一个引用同一缓冲区的字符串引用的老鼠窝。

tcomlyy6

tcomlyy63#

补充其他答案:
显然,Java标准类是这样做的:String.substring()返回的字符串重用原始字符串的内部字符数组(源代码,或者看看Sun的JDK源代码)。
问题是,这意味着在所有子字符串都符合GC条件之前,不能对原始String进行GC(因为它们共享后台字符数组)。如果从一个大字符串开始,从中提取一些较小的字符串,然后丢弃大字符串,这可能会导致内存浪费。例如,在解析输入文件时,这是常见的。
当然,一个聪明的GC可能会在值得的时候通过复制字符数组来解决这个问题(Sun JVM可能会这样做,我不知道),但是增加的复杂性可能是根本不实现这种共享行为的原因。

gywdnpxw

gywdnpxw4#

有很多方法可以实现像String这样的东西:
1.让一个“String”对象有效地包含一个数组,并暗示数组中的所有字符都在字符串中。这就是.net实际上所做的。
1.让每个“String”都是一个类,它包含一个数组引用,沿着有一个起始偏移量和长度。问题:创建大多数字符串需要示例化两个对象,而不是一个。
1.让每个“String”都是一个结构体,它包含一个数组引用沿着开始偏移量和长度。问题:数组到字符串类型字段将不再是原子的。
1.有两种或多种类型的“String”对象--那些包含数组中所有字符的对象,以及那些包含对另一个字符串的引用沿着偏移量和长度的对象。问题:这需要很多string的方法都是虚的。
1.让每个“String”都是一个特殊的类,它包括一个起始偏移量和长度,一个对可能是或可能不是同一个对象的对象引用,以及一个内置的字符数组。(因为它们都是),但允许相同的代码处理包含自己的字符或从其他字符串“借用”的字符串。
1.有一个通用的ImmutableArray类型(它将继承ReadableArray),并且有一个ImmutableArray可以与String互换。不可变数组有很多用途; String可能是最常见的用法,但不是唯一的。
1.像上面一样有一个通用的ImmutableArray类型,还有一个ImmutableArraySegment类,都继承自ImmutableArrayBase。这需要很多方法是虚拟的,这可能是我最喜欢的可能性。
请注意,这些方法中的大多数至少在某些使用场景中具有显著的限制。

h5qlskok

h5qlskok5#

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

a14dhokn

a14dhokn6#

在回顾了Substring方法和reflector之后,我发现如果你在Substriong方法中传递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;
}

字符串

1wnzp6jl

1wnzp6jl7#

这将增加intern表的复杂性(或者至少是更智能)。假设你已经在intern表中有两个条目“pending”和“bending”,以及以下代码:

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

字符串
实习生表中的哪个条目会被认为是命中?

相关问题