【问题标题】:Why does .NET create new substrings instead of pointing into existing strings?为什么 .NET 创建新的子字符串而不是指向现有字符串?
【发布时间】:2010-11-08 02:54:22
【问题描述】:

从使用反射器的简要介绍来看,String.Substring() 似乎为每个子字符串分配内存。我是正确的吗?我认为这没有必要,因为字符串是不可变的。

我的基本目标是创建一个不分配额外内存的IEnumerable<string> Split(this String, Char) 扩展方法。

【问题讨论】:

  • 我还没有仔细考虑过,或者用Reflector查看了StringBuilder的实现,但是IEnumerable Split(this StringBuilder, Char)方法会起作用吗?
  • 如果 String.Substring() 不分配新内存,字符串不会是不可变的

标签: c# .net string memory string-interning


【解决方案1】:

大多数具有不可变字符串的语言创建新子字符串而不是引用现有字符串的一个原因是因为这会干扰以后对这些字符串进行垃圾收集。

如果一个字符串被用作其子字符串,但随后较大的字符串变得不可访问(通过子字符串除外),会发生什么情况。较大的字符串将无法收集,因为这会使子字符串无效。短期内看来是节省内存的好方法,从长远来看却会导致内存泄漏。

【讨论】:

  • 我认为主要原因是关于字符串的算法。如果您可以安全地假设字符串永远不会更改,则可以安全地传递对它的引用,并且它本质上也是线程安全的。我想这也与垃圾收集有关。
  • @Spence - 这是不变性的原因。这不是避免字符串之间共享缓冲区的原因。一旦拥有了不变性和 GC,您就可以轻松地在后台实现共享缓冲区,而不会破坏线程安全或现有算法。
【解决方案2】:

如果不使用 String 类在 .net 内部进行探索,这是不可能的。您必须传递对可变数组的引用,并确保没有人搞砸。

.Net 会在您每次请求时创建一个新字符串。唯一的例外是由编译器创建(并且可以由您完成)的内部字符串,它们被放入内存一次,然后出于内存和性能原因建立指向该字符串的指针。

【讨论】:

    【解决方案3】:

    每个字符串都必须有自己的字符串数据,使用 String 类的实现方式。

    您可以创建自己的使用部分字符串的 SubString 结构:

    public struct SubString {
    
       private string _str;
       private int _offset, _len;
    
       public SubString(string str, int offset, int len) {
          _str = str;
          _offset = offset;
          _len = len;
       }
    
       public int Length { get { return _len; } }
    
       public char this[int index] {
          get {
             if (index < 0 || index > len) throw new IndexOutOfRangeException();
             return _str[_offset + index];
          }
       }
    
       public void WriteToStringBuilder(StringBuilder s) {
          s.Write(_str, _offset, _len);
       }
    
       public override string ToString() {
          return _str.Substring(_offset, _len);
       }
    
    }
    

    您可以使用其他方法来充实它,例如比较也可以在不提取字符串的情况下进行。

    【讨论】:

    • 一个子串变成另一个子串怎么样?
    • 是的,SubString 结构很容易创建另一个作为其自身一部分的结构。
    【解决方案4】:

    因为字符串在 .NET 中是不可变的,所以产生新字符串对象的每个字符串操作都会为字符串内容分配一个新的内存块。

    理论上,提取子字符串时可以重用内存,但这会使垃圾收集变得非常复杂:如果原始字符串被垃圾收集怎么办?共享一段的子字符串会发生什么?

    当然,没有什么能阻止 .NET BCL 团队在未来的 .NET 版本中改变这种行为。它不会对现有代码产生任何影响。

    【讨论】:

    • Java 的字符串实际上就是这样做的:子字符串只是指向原始字符串的指针。但是,这也意味着,当您从 200-MiB 字符串中提取 200 个字符的子字符串时,只要小子字符串没有被垃圾回收,那么 200-MiB 字符串就会一直存在于内存中。
    • 我认为它可能会影响现有代码,因为它是围绕这种行为设计的。如果人们假设他们的字符串将阻止其被复制并且此行为已停止,则可能会导致正在运行的应用程序因内存不足异常而停止。
    • 如何围绕这种行为进行设计?由于字符串的不变性,如果字符串类的内部实现发生变化,实际上没有办法创建会中断的代码。
    • .Net 字符串操作确实会创建新的字符串对象,但这不是 因为 字符串是不可变的。事实上,正是因为字符串是不可变的,所以字符串操作可以重用当前的字符串对象而不是创建新的对象。
    • 如果 C# 使用这种方法,它不会使垃圾收集有任何不同。原始字符串将有多个对其的引用,因此在基于它的所有子字符串也无法访问之前,它不会被垃圾收集。因此乔伊说了什么。 Java 具有更快的子字符串,可能会使用更高的内存,而 C# 具有较慢的子字符串,可能会更有效地使用内存。
    【解决方案5】:

    再加上Strings是不可变的,应该是后面的sn-p会在内存中生成多个String实例。

    String s1 = "Hello", s2 = ", ", s3 = "World!";
    String res = s1 + s2 + s3;
    

    s1+s2 => 新字符串实例(temp1)

    temp1 + s3 => 新字符串实例 (temp2)

    res 是对 temp2 的引用。

    【讨论】:

    • 这听起来像是编译器人员可以优化的东西。
    • 这不是编译器的问题,而是在设计语言时做出的选择。 Java 对字符串有相同的规则。 System.Text.StringBuilder 是一个模拟“可变”字符串的好类。
    • 错误 - s1 + s2 + s3 变成了对 String.Concat 的单个调用。这就是为什么最好使用 String.Format 或 StringBuilder (它们都比较慢),最多 4 个字符串。查看 IL 以了解编译器的作用,并使用分析器找出在您的程序中表现良好的部分。否则你可能会说“看,这是一只鞋!他已经脱掉了他的鞋,这表明其他跟随他的人也应该这样做!”请发布事实答案而不是神话答案。
    • 即Ian Boyd 的评论是正确的(除了编译器人员已经在版本 1 中处理了它。)
    • 根据 C# 语言参考,字符串上的 + 运算符定义为:string operator +(string x, string y);字符串运算符 +(字符串 x, 对象 y);字符串运算符 +(对象 x, 字符串 y);虽然运算符的实现可以使用 Concat 方法,但它不会改变 + 是二元运算符的事实;因此, s1 + s2 + s3 将等效于 String.Concat( String.Concat( s1, s2), s3) ,每次调用 Concat() 都会返回一个新的字符串对象
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-13
    相关资源
    最近更新 更多