StringBuilder.Append与StringBuilder.AppendFormat的区别

42

我对StringBuilder有些疑惑,希望社区能够解答我的问题。

先不考虑代码可读性,下面这两种方式哪个更,为什么呢?

StringBuilder.Append:

StringBuilder sb = new StringBuilder();
sb.Append(string1);
sb.Append("----");
sb.Append(string2);

StringBuilder.AppendFormat:


StringBuilder.AppendFormat方法将格式化字符串添加到当前实例中,并返回对此实例的引用。它使用与 String.Format 方法相同的参数和格式控制。
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0}----{1}",string1,string2);

5
每个给出绝对答案的评论都应该得到-1,因为他们没有考虑string1和string2的长度。你可以创建不同的测试,为string1和string2设定不同的值,这将使答案倾向于任一方向。 - casperOne
附加(Append)= 字符串格式化(String.Format)+ 附加(Append)。因此,.Append() 比 .AppendFormat() 更快更高效。这个基准测试已经过测试。请查看我的回答和测试结果。 - Konstantin Tarkus
许多以英语为第二语言的人似乎认为“doubt”等同于“question”,但实际上并非如此。除了怀疑者之外,没有人能够解释他们为什么怀疑。不过,我们当然可以回答你的问题! - Joel Mueller
10个回答

47
不知道string1string2的大小,所以很难说哪种方法更好。使用AppendFormat时,它只会预分配一次缓冲区,并根据格式字符串的长度和要插入的字符串将所有内容连接并插入缓冲区。对于非常大的字符串,这比多次调用Append要优越,因为后者可能会导致缓冲区多次扩展。
然而,对于三个调用Append,可能触发或不触发缓冲区的增长,并且每次都会执行该检查。如果字符串足够小且未触发缓冲区扩展,则它将比调用AppendFormat更快,因为它无需解析格式字符串以查找要进行替换的位置。
需要更多数据才能得出明确的答案。
应当指出,很少讨论在String类上使用静态Concat方法Jon的答案提到了使用AppendWithCapacity)。他的测试结果表明,这是最好的情况(假设您不必利用特定格式说明符)。 String.Concat通过相应的循环结构预先确定要连接的字符串的长度并分配缓冲区(由于参数中的循环结构略微增加开销),其性能将与Jon的AppendWithCapacity方法相当。
或者,可以直接使用加法运算符,因为它会编译为对String.Concat的调用,并且所有添加都在同一个表达式中:
// One call to String.Concat.
string result = a + b + c;

不是

// Two calls to String.Concat.
string result = a + b;
result = result + c;

针对那些提交测试代码的人

你需要在不同的运行中运行你的测试用例(或者至少在不同的测试运行之间执行垃圾回收)。原因是,如果你运行了100万次,每次迭代循环都创建一个新的StringBuilder,然后你运行下一个测试,它也循环相同数量的次数,创建了额外的 100万个StringBuilder实例,垃圾回收器很可能会在第二个测试期间介入并影响其计时。


+1 需要更多数据的观点很好。我认为我们大多数人在没有考虑到不同的输入会有重要性的情况下,就发表了一个明确的答案。 - Andrew Hare
+1 很好的观点。如果示例通过 Capacity 属性更改为预分配字符串空间,则差异仅在于字符串的格式。 - Kent Boogaart
@casperOne: 当然可以在程序运行期间调用GC.Collect()。虽然它不会完全一样,但差距很小。 - Jon Skeet
@Jon Skeet:已相应更新答案。 - casperOne
@casperOne:不,String.Concat并不完全做相同的事情——最终你会得到一个字符串,而不是一个 StringBuilder。我想在真正的代码中,你很可能会在之前或之后进行更多的追加操作。(cont) - Jon Skeet
显示剩余5条评论

26

casperOne正确。一旦达到某个阈值,Append()方法的速度会比AppendFormat()慢。以下是每种方法100,000次迭代的不同长度和经过的时间(以滴答为单位):

长度:1

Append()       - 50900
AppendFormat() - 126826

长度:1000

Append()       - 1241938
AppendFormat() - 1337396

长度:10,000

Append()       - 12482051
AppendFormat() - 12740862

长度:20,000

Append()       - 61029875
AppendFormat() - 60483914

当长度接近 20,000 的字符串被引入时,AppendFormat() 函数会略微优于 Append()

为什么会发生这种情况?请参见casperOne的答案

编辑:

我在发布配置下重新运行了每个测试,并更新了结果。


1
你能贴出那段代码吗?我想用一个预设的容量进行测试,但不想重复造轮子。 - Jon Skeet
这里的长度确切指的是什么?StringBuilder 的当前长度还是正在添加的字符串的长度? - Bacon Bits

14

casperOne正确指出这取决于数据。但是,假设您正在编写此作为类库供第三方使用 - 您会选择哪个呢?

一个选项是兼顾两者的优点 - 确定您实际需要添加多少数据,然后使用StringBuilder.EnsureCapacity确保我们只需要单个缓冲区调整大小。

如果我不是在意的话,我会使用Append x3 - 它似乎“更有可能”更快,因为在每次调用上解析字符串格式标记显然是无意义的工作。

请注意,我已要求BCL团队创建一种“缓存的格式化程序”,我们可以使用格式字符串创建并重复使用它。框架必须在每次使用时解析格式字符串,这太荒谬了。

编辑:好吧,我对John的代码进行了一些灵活性修改,并添加了一个“AppendWithCapacity”,它只是先计算所需容量。以下是不同长度的结果-对于长度1,我使用了1,000,000次迭代; 对于所有其他长度,我使用了100,000。 (这只是为了获得合理的运行时间。)所有时间均以毫秒为单位。

不幸的是,在SO中表格无法正常工作。长度分别为1、1000、10000、20000.

时间:

  • Append:162,475,7997,17970
  • AppendFormat:392,499,8541,18993
  • AppendWithCapacity:139,189,1558,3085

所以,我从未看到AppendFormat能够胜过Append - 但我确实看到AppendWithCapacity赢得了很大的优势。

这是完整的代码:

using System;
using System.Diagnostics;
using System.Text;

public class StringBuilderTest
{            
    static void Append(string string1, string string2)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append(string1);
        sb.Append("----");
        sb.Append(string2);
    }

    static void AppendWithCapacity(string string1, string string2)
    {
        int capacity = string1.Length + string2.Length + 4;
        StringBuilder sb = new StringBuilder(capacity);
        sb.Append(string1);
        sb.Append("----");
        sb.Append(string2);
    }

    static void AppendFormat(string string1, string string2)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendFormat("{0}----{1}", string1, string2);
    }

    static void Main(string[] args)
    {
        int size = int.Parse(args[0]);
        int iterations = int.Parse(args[1]);
        string method = args[2];

        Action<string,string> action;
        switch (method)
        {
            case "Append": action = Append; break;
            case "AppendWithCapacity": action = AppendWithCapacity; break;
            case "AppendFormat": action = AppendFormat; break;
            default: throw new ArgumentException();
        }

        string string1 = new string('x', size);
        string string2 = new string('y', size);

        // Make sure it's JITted
        action(string1, string2);
        GC.Collect();

        Stopwatch sw = Stopwatch.StartNew();
        for (int i=0; i < iterations; i++)
        {
            action(string1, string2);
        }
        sw.Stop();
        Console.WriteLine("Time: {0}ms", (int) sw.ElapsedMilliseconds);
    }
}

@Jon Skeet:我还没有看过它,但编译后的Regex可能是缓存格式化程序想法的解决方案吗?我希望它足够聪明,能够预先分配输出,并且可以防止每次解析格式字符串。 - casperOne
@Jon Skeet:它不会进行解析,但这不是问题的一半吗?如果你要进行简单的替换,那就不是什么大问题。然而,当使用Regex时,你无法获得存储格式字符串的效果(特别是如果你想格式化参数)。 - casperOne
@casperOne:我仍然不明白你如何进行替换。但是如果没有格式化字符串的能力,它就真的不等同了。StringFormatter类型肯定会很方便。 - Jon Skeet
@Jon:我记得你有一段时间提到过你的“缓存格式”想法,我一直在考虑如何实现自己的版本。当然,为了真正有用,它需要_完全_匹配普通的String.Format()解析,并且我不能在工作中使用反射器:( - Joel Coehoorn
@Jon Skeet:你的“AppendWithCapacity”实际上就是“String.Concat”,所以不需要那个多余的混乱代码。 - casperOne
显示剩余3条评论

6

Append大多数情况下会更快,因为该方法有许多重载,使编译器能够调用正确的方法。由于您正在使用Strings,所以StringBuilder可以使用AppendString重载。

AppendFormat接受一个String,然后是一个Object[],这意味着格式必须被解析,并且数组中的每个Object都必须进行ToString'd,然后才能添加到StringBuilder's内部数组中。

注意:根据casperOne的观点——如果没有更多数据,很难给出确切的答案。


2

StringBuilder 还具有级联追加功能:Append() 返回 StringBuilder 本身,因此您可以像这样编写代码:

StringBuilder sb = new StringBuilder();
sb.Append(string1)
  .Append("----")
  .Append(string2);

代码更加简洁,生成的IL代码较少(虽然这只是微小的优化)。


1

当然,每种情况都需要了解具体情况。

话虽如此,我认为通常情况下应该是前者,因为您不会重复解析格式字符串。

但是,差异非常小。实际上,在大多数情况下,您真的应该考虑使用AppendFormat


0
我会认为是调用执行最少工作的那个。Append只是简单地连接字符串,而AppendFormat则进行字符串替换。当然,现在的情况很难说...

-1,Append不会连接字符串。它是将字符添加到其内部字符数组中。 - Samuel

0

1 应该更快,因为它只是简单地附加字符串,而 2 必须根据格式创建一个字符串,然后再附加字符串。因此,其中有一个额外的步骤。


0

在你的情况下,“更快”是第一位,但这并不是一个公平的比较。你应该问问 StringBuilder.AppendFormat()StringBuilder.Append(string.Format()) 之间的区别——其中第一个由于内部使用 char 数组而更快。

然而,你的第二个选项更易读。


string.Format() 在内部创建一个 StringBuilder 对象,因此 StringBuilder.AppendFormat() 基本上与 string.Format() 相同。 - Sergio
虽然这可能是正确的,但在执行Append(string.Format())时涉及两个步骤 - 首先,必须将格式化内容复制到StringBuilder的内容中。而使用AppendFormat只需要一步。 - Miha Markic
1
这就是为什么我从一开始就没有考虑“StringBuilder.Append(string.Format())”选项的原因 ;) - Sergio

0
在C# 10/.NET 6+中,由于编译器内置的新的插值字符串处理程序,这两个代码示例都会编译成相同的代码。事实上,所有这些都会产生等效的编译代码:
sb.Append(string1);
sb.Append("----");
sb.Append(string2);

sb.AppendFormat("{0}----{1}",string1,string2);

sb.Append($"{string1}----{string2}");

因此,你可以选择最易读的方式。

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