【问题标题】:Does string.Replace(string, string) create additional strings?string.Replace(string, string) 会创建额外的字符串吗?
【发布时间】:2014-12-05 14:16:47
【问题描述】:

我们需要将包含dd/mm/yyyy 格式的日期的字符串转换为ddmmyyyy 格式(如果您想知道我为什么将日期存储在字符串中,我的软件会处理批量交易文件,这是一个银行使用的基于行的文本文件格式)。

我目前正在这样做:

string oldFormat = "01/01/2014";
string newFormat = oldFormat.Replace("/", "");

果然,这会将"01/01/2014" 转换为"01012014"。但我的问题是,替换是一步完成的,还是会创建一个中间字符串(例如:"0101/2014""01/012014")?


这就是我问这个的原因:

我正在处理大小从几千字节到数百兆字节不等的事务文件。到目前为止,我还没有遇到性能/内存问题,因为我仍在使用非常小的文件进行测试。但是当涉及到兆字节时,我不确定这些额外的字符串是否会出现问题。我怀疑会是这样,因为strings are immutable。对于数百万条记录,这种额外的内存消耗将大大增加。

我已经在使用StringBuilders 来创建输出文件。而且我也知道discarded strings will be garbage collected(在时间结束前的某个时间点)。我想知道是否有更好、更有效的方法来替换字符串中所有出现的特定字符/子字符串,这种方法不会另外创建字符串。

【问题讨论】:

  • 您应该尝试使用 Regex.Replace,并比较性能。我曾经不得不从大小约为 1MB 的文件中删除不必要的 NewLine 字符,而正则表达式有很大的不同(以分钟为单位......)虽然我必须进行条件替换和其他一些文本操作,所以我建议在这个确切的情况
  • 我认为它只为整个替换分配一个字符串。每次替换都不是一个字符串。
  • String ReplaceInternal 是外部实现的方法。我认为我们无法知道幕后发生的事情。

标签: c# .net


【解决方案1】:

我还没有找到任何来源,但我强烈怀疑该实现是否总是创建新字符串。我也会在内部使用 StringBuilder 来实现它。然后String.Replace 绝对可以,如果你想替换一个巨大的字符串。但是如果你必须多次替换它,你应该考虑使用StringBuilder.Replace,因为每次调用Replace都会创建一个新字符串。

因此您可以使用StringBuilder.Replace,因为您已经在使用StringBuilder

【讨论】:

  • 谢谢,原来我的问题是XY problem,你给了一个很好的技巧来解决X(有效替换)。但我也想知道 Y 的答案(如果替换多次出现会创建多个字符串)。
  • @Krumia:我还没有找到任何来源,但我强烈怀疑实现总是会创建新的字符串。我也会在内部使用 StringBuilder 来实现它。然后String.Replace 如果你想替换一个巨大的字符串,那绝对没问题。但是如果你必须多次替换它,你应该考虑使用StringBuilder.Replace,因为每次调用Replace都会创建一个新字符串(我会将此评论添加到我的答案中)。
【解决方案2】:

好吧,我不是 .NET 开发团队的成员(很遗憾),但我会尽力回答您的问题。

Microsoft 有一个很棒的 .NET 参考源代码站点,according to itString.Replace 调用了一个外部方法来完成这项工作。我不会争论它是如何实现的,但是对这个方法有一个小评论可以回答你的问题:

// This method contains the same functionality as StringBuilder Replace. The only difference is that
// a new String has to be allocated since Strings are immutable

现在,如果我们遵循 StringBuilder.Replace 的实现,我们将看到它在内部的实际作用。

关于字符串对象的更多信息

虽然String 在.NET 中是不可变的,但这不是某种限制,它是一个契约。 String实际上是一个引用类型,它包含的是实际字符串的长度+字符的缓冲区。您实际上可以获取指向此缓冲区的不安全指针并“即时”更改它,但我不建议这样做。

现在,StringBuilder 类还包含一个字符数组,当您将字符串传递给它的构造函数时,它实际上会将字符串的缓冲区复制到他自己的缓冲区(参见参考源代码)。但是,它所没有的是不变性契约,因此当您使用 StringBuilder 修改字符串时,您实际上是在使用 char 数组。请注意,当您在 StringBuilder 上调用 ToString() 时,它会创建一个新的“不可变”字符串,并将其缓冲区复制到那里。

因此,如果您需要一种快速且节省内存的方法来更改字符串,那么 StringBuilder 绝对是您的选择。如果您“对字符串进行重复修改”,尤其是关于 Microsoft 明确 recommends 使用 StringBuilder。

【讨论】:

  • String.Replace 的合同不要求实现避免创建不必要的中间 String 对象,但是当它很容易避免时,不太可能使用这样的实现。
  • 所以我的答案几乎和你一样,我在你之前回答......你投赞成票,我投反对票......什么给出了??
  • @kjbartel:您以什么方式回答甚至与此类似?你说它总是创建一个新字符串。但是 OP 询问它是否为应该替换的字符串的每次出现创建一个新字符串,而不是每个 Replace-call 一次。这试图找到一个来源,其中记录了String.Replace 的实际实现方式。该评论表明只创建了一个字符串。
  • @SamHarwell 我不会争论实际的实现,因为它很可能在本机代码中实现,但它绝对不会创建新的中间字符串。实际上,如果您 “对字符串执行重复修改”,Microsoft 本身 recommends 会使用 StringBuilder。
【解决方案3】:

没有字符串方法。你是你自己的。但你可以尝试这样的事情:

oldFormat="dd/mm/yyyy";

string[] dt = oldFormat.Split('/');
string newFormat = string.Format("{0}{1}/{2}", dt[0], dt[1], dt[2]);

StringBuilder sb = new StringBuilder(dt[0]);
sb.AppendFormat("{0}/{1}", dt[1], dt[2]);

【讨论】:

    【解决方案4】:

    果然,这会将“01/01/2014”转换为“01012014”。但我的问题 是,替换是一步发生的,还是创建一个 中间字符串(例如:“0101/2014”或“01/012014”)?

    ,它不会为每个替换创建中间字符串。但它确实会创建新字符串,因为如您所知,字符串是不可变的。

    为什么?

    没有理由在每次替换时都创建新字符串 - 避免它非常简单,并且会极大地提升性能。

    如果您非常感兴趣referencesource.microsoft.comSSCLI2.0 源代码将演示这一点(how-to-see-code-of-method-which-marked-as-methodimploptions-internalcall):

    FCIMPL3(Object*, COMString::ReplaceString, StringObject* thisRefUNSAFE, 
              StringObject* oldValueUNSAFE, StringObject* newValueUNSAFE)
    {
    
       // unnecessary code ommited
          while (((index=COMStringBuffer::LocalIndexOfString(thisBuffer,oldBuffer,
                 thisLength,oldLength,index))>-1) && (index<=endIndex-oldLength))
        {
            replaceIndex[replaceCount++] = index;
            index+=oldLength;
        }
    
        if (replaceCount != 0)
        {
            //Calculate the new length of the string and ensure that we have 
            // sufficent room.
            INT64 retValBuffLength = thisLength - 
                ((oldLength - newLength) * (INT64)replaceCount);
    
            gc.retValString = COMString::NewString((INT32)retValBuffLength);
         // unnecessary code ommited
        }
    }
    

    如你所见,retValBuffLength 被计算出来,它知道replaceCount 的数量。 .NET 4.0 的实际实现可能会有些不同(SSCLI 4.0 未发布),但我向您保证它并没有做任何愚蠢的事情:-)。

    我想知道是否有更好、更有效的替换方式 字符串中所有出现的特定字符/子字符串,即 不会另外创建字符串。

    是的。可重复使用的StringBuilder,容量约为 2000 个字符。避免任何内存分配。仅当替换长度相等时才适用,并且如果您处于紧密循环中,则可以为您带来不错的性能提升。

    在编写任何内容之前,请使用大文件运行基准测试,看看性能是否足以满足您的需求。如果性能足够 - 不要做任何事情。

    【讨论】:

    • @Alovchin,是的,几个小时前我自己发现了它。它只有 2.0,但绝对能让你很好地了解发生了什么:-)
    • @ChrisEelmaa 您是如何发现String.ReplaceInternal method 在 CLI 2.0 上调用此代码的?
    • @SonerGönül:编辑了我的帖子并添加了说明。截至目前,看到String.ReplaceInternal 的唯一机会就是拆解你的mscorlib.dll。 SSCLI2.0 已经足够好,虽然可以争论这个问题。 grepWin 是你的朋友 ;)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-15
    • 2011-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-28
    相关资源
    最近更新 更多