何时使用StringBuilder?

95

我了解StringBuilder的好处。

但是如果我想连接2个字符串,那么我认为最好(更快)不使用StringBuilder。这正确吗?

在什么情况下(字符串数量)使用StringBuilder会更好?


1
我相信这个之前已经讨论过了。 - Mark Schultheiss
2
可能的重复问题:https://dev59.com/_3RB5IYBdhLWcg3wvpic,https://dev59.com/hnRB5IYBdhLWcg3wr48V,https://dev59.com/lnI-5IYBdhLWcg3w6tFR,https://dev59.com/d3VD5IYBdhLWcg3wTZ1m,https://dev59.com/RHVD5IYBdhLWcg3wI4CM。 - Peter Mortensen
可能是https://dev59.com/_3RB5IYBdhLWcg3wvpic的重复问题。 - Jørn Schou-Rode
12个回答

89

我强烈建议您阅读Jeff Atwood的The Sad Tragedy of Micro-Optimization Theater,该文章与Simple Concatenation vs. StringBuilder以及其他方法有关。

现在,如果您想查看一些数字和图表,请点击链接;)


8
你的阅读有误,然而这在很多情况下并不重要,特别是没有循环涉及的情况下。但在其他情况下,它可能非常重要。 - Peter
1
我已经删除了编辑,因为在一个被接受的答案中只是错误的信息。 - Peter
2
仅为了展示其重要性,从您所引用的文章中可以得知:“在大多数垃圾回收语言中,字符串是不可变的:当您添加两个字符串时,两者的内容都会被复制。随着您在此循环中不断添加,每次都会分配更多的内存。这直接导致了可怕的二次n2性能。” - Peter
我尝试使用StringBuilder,但问题在于我仍然必须调用ToString才能将其与代码中其他字符串进行比较,这似乎使其更加低效。 - James Joshua Street
4
为什么这个回答被接受了?我认为简单地放一个链接并说“去看这个”并不是一个好的回答。 - Kellen Stuart
显示剩余2条评论

53

但如果我想连接两个字符串,那么我认为最好(更快)不使用 StringBuilder。这是正确的吗?

确实如此,你可以在以下链接中找到更详细的解释:

关于字符串和 StringBuilder 的文章

总结一下:如果你可以像下面这样一次性连接字符串:

var result = a + " " + b  + " " + c + ..

如果只需要进行一次字符串拼接,那么使用StringBuilder可能不是最优的选择(因为结果字符串的长度可以预先计算得出);

对于类似结构的情况

var result = a;
result  += " ";
result  += b;
result  += " ";
result  += c;
..
每次都会创建新的对象,因此你应该考虑使用StringBuilder。
文章总结了以下经验规则:
经验法则:
当你在非平凡循环中进行连接操作时,尤其是如果你无法确定(在编译时)将通过循环执行多少次,则一定要使用StringBuilder。例如,按字符读取文件,并使用+=运算符逐步构建字符串可能会导致性能问题。
当您可以(易读地)指定需要连接的所有内容时,一定要使用连接运算符。(如果您有一个需连接的东西数组,请考虑显式调用String.Concat - 或者如果您需要分隔符,则使用String.Join。)
不要害怕将文字字面量拆分为多个连接位-结果将相同。您可以通过将长文字拆分为多行来提高可读性,例如,而没有损害性能。
如果您需要连接的中间结果用于除了提供连接的下一轮迭代之外的其他用途,则StringBuilder对您没有帮助。例如,如果您从名字和姓氏构建全名,然后在末尾添加第三个信息(例如昵称),则仅在您不需要(名字+姓氏)字符串以供其他用途时,使用StringBuilder才有益(正如我们在创建Person对象的示例中所做的那样)。
如果您只需要做一些连接操作,并且您确实想在单独的语句中执行连接操作,则无论您选择哪种方法都不重要。哪种方法更有效将取决于连接的数量,所涉及字符串的大小以及它们连接的顺序。如果您真的认为该代码片段是性能瓶颈,请分析或测试两种方式。

链接已经失效。很好,你复制粘贴了经验法则。 - carloswm85
这是链接:https://web.archive.org/web/20181106113504/http://yoda.arachsys.com:80/csharp/stringbuilder.html - carloswm85

17

System.String是一个不可变对象-这意味着每当您修改它的内容时,它都会分配一个新字符串,这需要时间(和内存?)。 使用StringBuilder,您可以修改对象的实际内容而无需分配新的字符串。

因此,当您需要对字符串进行多次修改时,请使用StringBuilder。


9

其实并不是这样的...如果你需要连接大量字符串或者有多次连接,比如在循环中,那么你应该使用StringBuilder。


1
那是错误的。只有在循环或连接操作影响性能时,才应该使用 StringBuilder - Alex Bagnolini
2
@Alex:这不是一直都这样吗?;) 不过,说真的,在循环内部进行连接操作时,我总是使用StringBuilder...虽然我的循环都有超过1k次迭代... @Binary:通常情况下,这应该编译为string s = "abcd",至少这是我最后听到的...不过,对于变量来说,最可能的是使用Concat。 - Bobby
1
事实是:几乎总是不是这种情况。我总是使用字符串操作符 a + "hello" + "somethingelse",从来不用担心它。如果它成为问题,我会使用 StringBuilder。但我一开始就没有担心它,写它花费的时间更少。 - Alex Bagnolini
3
大字符串并不能提高性能,只有进行多次字符串连接时才有性能的提升。 - Konrad Rudolph
1
@Konrad:你确定没有性能优势吗?每次连接大字符串时,都会复制大量数据;每次连接小字符串时,只会复制少量数据。 - LukeH
显示剩余3条评论

6
  • 如果您在循环中连接字符串,应考虑使用StringBuilder而不是普通的String
  • 如果只是单个字符串拼接,执行时间可能根本看不出差异

这是一个简单的测试应用程序,以证明这一点:

static void Main(string[] args)
    {
        //warm-up rounds:
        Test(500);
        Test(500);

        //test rounds:
        Test(500);
        Test(1000);
        Test(10000);
        Test(50000);
        Test(100000);

        Console.ReadLine();
    }

    private static void Test(int iterations)
    {
        int testLength = iterations;
        Console.WriteLine($"----{iterations}----");

        //TEST 1 - String
        var startTime = DateTime.Now;
        var resultString = "test string";
        for (var i = 0; i < testLength; i++)
        {
            resultString += i.ToString();
        }
        Console.WriteLine($"STR: {(DateTime.Now - startTime).TotalMilliseconds}");



        //TEST 2 - StringBuilder
        startTime = DateTime.Now;
        var stringBuilder = new StringBuilder("test string");
        for (var i = 0; i < testLength; i++)
        {
            stringBuilder.Append(i.ToString());
        }
        string resultString2 = stringBuilder.ToString();
        Console.WriteLine($"StringBuilder: {(DateTime.Now - startTime).TotalMilliseconds}");


        Console.WriteLine("---------------");
        Console.WriteLine("");

    }

结果(以毫秒为单位):

----500----
STR: 0.1254
StringBuilder: 0
---------------

----1000----
STR: 2.0232
StringBuilder: 0
---------------

----10000----
STR: 28.9963
StringBuilder: 0.9986
---------------

----50000----
STR: 1019.2592
StringBuilder: 4.0079
---------------

----100000----
STR: 11442.9467
StringBuilder: 10.0363
---------------

公平起见,您应该在最后执行 stringBuilder.ToString() 以使比较公平。 - nawfal
是的,你说得对。 - adrian.krzysztofek
我已经更新了我的答案,使两种情况更加平衡。 - adrian.krzysztofek

6

为了表述清晰,我一般使用字符串构建器来连接三个或更多的字符串。

然后你要数到三,不多也不少。三应该是你要数的数字,而且计数的数字也应该是三。你不应该数四,也不应该数两个,除非你随后继续数到三。一旦达到第三个数字,即第三个数字,那么你就可以扔出你的安条克圣手榴弹了。


这取决于情况:仅串联会生成一份副本: "Russell" + " " + Steen + ".",只会生成一份副本,因为它先计算字符串的长度。只有当你必须分割你的字符串时,你才应该考虑使用构建器。 - Peter

5

由于很难找到不受意见影响或跟随自豪心理的解释,所以我想在LINQpad上编写一些代码来测试它。

我发现使用小型字符串而不是使用i.ToString()可以改变响应时间(在小循环中可见)。

该测试使用不同的迭代序列以使时间测量在合理可比范围内。

我会在最后复制代码,这样您就可以尝试自己(results.Charts...Dump()无法在LINQPad之外工作)。

输出(X轴:测试迭代次数,Y轴:消耗的时间,单位为Tick)

迭代序列:2, 3, 4, 5, 6, 7, 8, 9, 10 迭代序列:2, 3, 4, 5, 6, 7, 8, 9, 10

迭代序列:10, 20, 30, 40, 50, 60, 70, 80 迭代序列:10, 20, 30, 40, 50, 60, 70, 80

迭代序列:100, 200, 300, 400, 500 迭代序列:100, 200, 300, 400, 500

代码(使用LINQPad 5编写):

void Main()
{
    Test(2, 3, 4, 5, 6, 7, 8, 9, 10);
    Test(10, 20, 30, 40, 50, 60, 70, 80);
    Test(100, 200, 300, 400, 500);
}

void Test(params int[] iterationsCounts)
{
    $"Iterations sequence: {string.Join(", ", iterationsCounts)}".Dump();
    
    int testStringLength = 10;
    RandomStringGenerator.Setup(testStringLength);
    var sw = new System.Diagnostics.Stopwatch();
    var results = new Dictionary<int, TimeSpan[]>();
        
    // This call before starting to measure time removes initial overhead from first measurement
    RandomStringGenerator.GetRandomString(); 
        
    foreach (var iterationsCount in iterationsCounts)
    {
        TimeSpan elapsedForString, elapsedForSb;
        
        // string
        sw.Restart();
        var str = string.Empty;

        for (int i = 0; i < iterationsCount; i++)
        {
            str += RandomStringGenerator.GetRandomString();
        }
        
        sw.Stop();
        elapsedForString = sw.Elapsed;


        // string builder
        sw.Restart();
        var sb = new StringBuilder(string.Empty);

        for (int i = 0; i < iterationsCount; i++)
        {
            sb.Append(RandomStringGenerator.GetRandomString());
        }
        
        sw.Stop();
        elapsedForSb = sw.Elapsed;

        results.Add(iterationsCount, new TimeSpan[] { elapsedForString, elapsedForSb });
    }


    // Results
    results.Chart(r => r.Key)
    .AddYSeries(r => r.Value[0].Ticks, LINQPad.Util.SeriesType.Line, "String")
    .AddYSeries(r => r.Value[1].Ticks, LINQPad.Util.SeriesType.Line, "String Builder")
    .DumpInline();
}

static class RandomStringGenerator
{
    static Random r;
    static string[] strings;
    
    public static void Setup(int testStringLength)
    {
        r = new Random(DateTime.Now.Millisecond);
        
        strings = new string[10];
        for (int i = 0; i < strings.Length; i++)
        {
            strings[i] = Guid.NewGuid().ToString().Substring(0, testStringLength);
        }
    }
    
    public static string GetRandomString()
    {
        var indx = r.Next(0, strings.Length);
        return strings[indx];
    }
}

5

没有确定的答案,只有经验法则。我的个人规则大致如下:

  • 如果在循环中连接字符串,请始终使用 StringBuilder
  • 如果字符串很大,请始终使用 StringBuilder
  • 如果连接代码在屏幕上整洁易读,则可能可以使用。
    如果不是这样,请使用 StringBuilder

我知道这是一个老话题,但我现在才开始学习,想知道你认为什么是“大字符串”? - MatthewD

4
如果我想要连接两个字符串,那么在没有使用 StringBuilder 的情况下这样做更好、更快。这是正确的吗?
是的。但更重要的是,在这种情况下使用普通的 String 更加易读。另一方面,在循环中使用它是有意义的,也可以和连接一样易读。
我会谨慎对待那些引用具体连接数量作为阈值的经验法则。仅在循环中使用它可能同样有用、更易记,并且更有意义。

“我会对那些引用特定的连接数量作为门槛的经验法则保持警惕。”在应用常识后,考虑一下6个月后回到你的代码的人。 - Phil Cooper

3

单个字符串拼接没有必要使用StringBuilder。通常我会以5个字符串拼接为一个经验法则。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接