【问题标题】:Why doesn't string.Substring share memory with the source string?为什么 string.Substring 不与源字符串共享内存?
【发布时间】:2011-09-10 14:19:18
【问题描述】:

众所周知,.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* 指针,对吧?所以在我看来,至少在理论上应该允许以下内容分配一个 single 内存块来保存 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

【问题讨论】:

  • substring 创建基本字符串的新实例,不是吗?
  • 在子字符串文档中:“此方法不会修改当前实例的值。相反,它返回一个从当前字符串的 startIndex 位置开始的新字符串。”我会说它永远不应该像你的第一个例子那样表现。如果您使用子字符串,那么应该期望创建不同的实例以进行进一步修改。
  • 只是问...当你在类不变量中偷偷摸摸时,你真的希望 anything 工作吗?
  • 为什么 .net 框架不将字母表的所有排列存储在内存中,而我们只是引用指向我们需要的部分的指针? :-)

标签: c# .net string substring immutability


【解决方案1】:

可以通过多种方式实现类似 String 的内容:

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

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

【讨论】:

    【解决方案2】:

    添加到其他答案:

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

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

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

    【讨论】:

    • +1 以避免增加的复杂性。这是我最近经常想到的事情:我认为在很多情况下,我比以前更喜欢“愚蠢、明显”的解决方案,而不是聪明、不易证明的想法。
    • @Dan Tao:是的,只是我的想法。编程时,“聪明”往往是一件坏事。
    【解决方案3】:

    这会增加实习生表的复杂性(或至少更聪明)。假设您已经在实习生表“pending”和“bending”中有两个条目,以及以下代码:

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

    实习生表中的哪个条目会被视为命中?

    【讨论】:

    • 两者都不是。在运行时创建的字符串不会自动被实习。
    【解决方案4】:

    在使用反射器查看 Substring 方法后,我发现如果您在 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;
    }
    

    【讨论】:

    • 是的...这基本上就是我在第一个示例中试图展示的内容。问题是为什么当您传递一个 非零 值时,返回的 string 对象与原始对象在内存中不共享相同的 char 值。
    • 也许这个链接可以帮助stackoverflow.com/questions/636932/…
    【解决方案5】:

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

    【讨论】:

    • 嗯,当然……我从来没有说过应该。从技术的角度来看,我只是好奇为什么会做出这个决定。我认为 Guffa 和 Joe 给出了一些很好的理由。
    • 你是对的,这是你通常不应该打扰自己的细节。但是,为了更好地了解它的用途,讨论如何构建语言的内部结构仍然很有价值,这样您就可以避免本质上无效的事情。
    【解决方案6】:

    我相信 C# 字符串是空终止的 - 虽然这是一个不应该涉及托管消费者的实现细节,但在某些情况下(例如编组)它很重要。

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

    【讨论】:

    • 这也是一个很好的答案;谢谢!考虑到这些点后,这很有意义。
    • C# 字符串不是以空结尾的,这很容易证明。 "abc\0def".Length7 而不是 3 (如果它们被空终止会是什么)
    • @wischi - 我所说的“空终止”是我认为在底层内存缓冲区中的字符串字符后面有一个空('\0')字符。并不是说它是经典 C 意义上的“空终止”,即字符串由它在其缓冲区中包含的第一个空字符终止。 Guffa 的回答说同样的话,但更清楚,并且是正确的接受的答案。
    【解决方案7】:

    有两个原因:

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

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

    【讨论】:

    • 我怀疑这有一些很好的理由;果然,有!感谢您的洞察力。
    猜你喜欢
    • 1970-01-01
    • 2011-01-06
    • 1970-01-01
    • 2012-03-07
    • 2010-11-10
    • 2014-04-27
    • 1970-01-01
    • 2014-10-08
    • 2015-05-02
    相关资源
    最近更新 更多