161

假设我在 C# 中有一个 stringbuilder 执行此操作:

StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();

这是否会像拥有以下内容一样有效或更有效:

string cat = "cat";
string s = String.Format("The {0} in the hat", cat);

如果是这样,为什么?

编辑

在一些有趣的答案之后,我意识到我可能应该更清楚我所问的问题。我并没有太多要求哪个在连接字符串时更快,但在一个字符串注入另一个字符串时哪个更快。

在上述两种情况下,我都想将一个或多个字符串注入到预定义模板字符串的中间。

对困惑感到抱歉

4

12 回答 12

148

注意:这个答案是在 .NET 2.0 是当前版本时编写的。这可能不再适用于更高版本。

String.Format在内部使用 a StringBuilder

public static string Format(IFormatProvider provider, string format, params object[] args)
{
    if ((format == null) || (args == null))
    {
        throw new ArgumentNullException((format == null) ? "format" : "args");
    }

    StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
    builder.AppendFormat(provider, format, args);
    return builder.ToString();
}

上面的代码是来自 mscorlib 的一个片段,所以问题变成了“StringBuilder.Append()StringBuilder.AppendFormat()”快?

如果没有基准测试,我可能会说上面的代码示例使用.Append(). 但这是一个猜测,请尝试对两者进行基准测试和/或分析以获得适当的比较。

这个小伙子 Jerry Dixon 做了一些基准测试:

http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

更新:

可悲的是,上面的链接已经死了。然而,在返程机器上仍然有一份副本:

http://web.archive.org/web/20090417100252/http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

归根结底,这取决于您的字符串格式是否会被重复调用,即您正在对超过 100 兆字节的文本进行一些严肃的文本处理,或者当用户不时单击按钮时是否会调用它。除非你正在做一些巨大的批处理工作,否则我会坚持使用 String.Format,它有助于代码的可读性。如果您怀疑性能瓶颈,请在您的代码上粘贴一个分析器,看看它到底在哪里。

于 2008-08-09T15:57:15.970 回答
45

MSDN 文档

String 或 StringBuilder 对象的连接操作的性能取决于内存分配发生的频率。String 连接操作总是分配内存,而 StringBuilder 连接操作仅在 StringBuilder 对象缓冲区太小而无法容纳新数据时才分配内存。因此,如果串联固定数量的 String 对象,则 String 类更适合串联操作。在这种情况下,编译器甚至可以将各个连接操作组合成一个操作。如果串联任意数量的字符串,则 StringBuilder 对象更适合串联操作;例如,如果一个循环连接随机数量的用户输入字符串。

于 2008-08-09T14:36:45.290 回答
12

我运行了一些快速的性能基准测试,对于 10 次运行的平均 100,000 次操作,第一种方法(字符串生成器)花费的时间几乎是第二种方法(字符串格式)的一半。

所以,如果这种情况很少发生,那也没关系。但如果是普通操作,那么你可能要使用第一种方法。

于 2008-08-09T19:34:51.567 回答
10

我希望String.Format会更慢 - 它必须解析字符串然后连接它。

几点注意事项:

  • 格式是在专业应用程序中获取用户可见字符串的方法;这避免了本地化错误
  • 如果您事先知道结果字符串的长度,请使用StringBuilder(Int32)构造函数来预定义容量
于 2008-08-09T17:09:10.880 回答
8

我认为在大多数情况下,您最关心的是这种清晰度,而不是效率。除非您将成吨的绳子压在一起,或者为低功率的移动设备构建一些东西,否则这可能不会对您的运行速度产生太大影响。

我发现,在我以相当线性的方式构建字符串的情况下,直接连接或使用 StringBuilder 是您的最佳选择。如果您正在构建的大部分字符串是动态的,我建议这样做。由于很少有文本是静态的,因此最重要的是清楚每条动态文本的放置位置,以防将来需要更新。

另一方面,如果你谈论的是一大块包含两个或三个变量的静态文本,即使它的效率有点低,我认为你从 string.Format 获得的清晰度让它值得。本周早些时候,当我不得不在 4 页文档的中心放置一点动态文本时,我使用了它。如果将一大段文本更新为一个片段,则比必须更新连接在一起的三个片段更容易。

于 2008-08-09T14:40:00.650 回答
8

如果只是因为 string.Format 并没有完全按照您的想法做,那么这里是 6 年后在 Net45 上重新运行的测试。

Concat 仍然是最快的,但实际上相差不到 30%。StringBuilder 和 Format 仅相差 5-10%。运行测试几次,我得到了 20% 的变化。

毫秒,一百万次迭代:

  • 串联:367
  • 每个键的新 stringBuilder:452
  • 缓存的 StringBuilder:419
  • 字符串。格式:475

我学到的教训是性能差异是微不足道的,因此它不应该阻止您编写最简单的可读代码。这对我来说是经常但并非总是如此a + b + c

const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);

var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];

var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();

for(int i=0; i<iterations; i++){
    var key1= keyprefix+":" + i.ToString();
    concatkeys[i]=key1;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
    stringbuilderkeys[i]= key2;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
    cachedsbkeys[i]= key2b;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("string.Format");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
    formatkeys[i]= key3;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);
于 2015-01-12T12:52:46.433 回答
6

String.Format在内部使用StringBuilder,因此在逻辑上导致了这样一种想法,即由于更多的开销,它的性能会有所降低。但是,在很大程度上,简单的字符串连接是在其他两个字符串之间注入一个字符串的最快方法。几年前,Rico Mariani 在他的第一次表演测验中证明了这一证据。简单的事实是,当字符串部分的数量已知时(没有限制——你可以连接一千个部分,只要你知道它总是 1000 个部分),连接总是比StringBuilderor快String.Format。它们可以通过单个内存分配和一系列内存副本来执行。是证据。

下面是一些String.Concat方法的实际代码,这些方法最终调用FillStringChecked,它使用指针来复制内存(通过反射器提取):

public static string Concat(params string[] values)
{
    int totalLength = 0;

    if (values == null)
    {
        throw new ArgumentNullException("values");
    }

    string[] strArray = new string[values.Length];

    for (int i = 0; i < values.Length; i++)
    {
        string str = values[i];
        strArray[i] = (str == null) ? Empty : str;
        totalLength += strArray[i].Length;

        if (totalLength < 0)
        {
            throw new OutOfMemoryException();
        }
    }

    return ConcatArray(strArray, totalLength);
}

public static string Concat(string str0, string str1, string str2, string str3)
{
    if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
    {
        return Empty;
    }

    if (str0 == null)
    {
        str0 = Empty;
    }

    if (str1 == null)
    {
        str1 = Empty;
    }

    if (str2 == null)
    {
        str2 = Empty;
    }

    if (str3 == null)
    {
        str3 = Empty;
    }

    int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
    string dest = FastAllocateString(length);
    FillStringChecked(dest, 0, str0);
    FillStringChecked(dest, str0.Length, str1);
    FillStringChecked(dest, str0.Length + str1.Length, str2);
    FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
    return dest;
}

private static string ConcatArray(string[] values, int totalLength)
{
    string dest = FastAllocateString(totalLength);
    int destPos = 0;

    for (int i = 0; i < values.Length; i++)
    {
        FillStringChecked(dest, destPos, values[i]);
        destPos += values[i].Length;
    }

    return dest;
}

private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
    int length = src.Length;

    if (length > (dest.Length - destPos))
    {
        throw new IndexOutOfRangeException();
    }

    fixed (char* chRef = &dest.m_firstChar)
    {
        fixed (char* chRef2 = &src.m_firstChar)
        {
            wstrcpy(chRef + destPos, chRef2, length);
        }
    }
}

那么,那么:

string what = "cat";
string inthehat = "The " + what + " in the hat!";

享受!

于 2009-05-23T21:33:21.450 回答
3

哦,最快的是:

string cat = "cat";
string s = "The " + cat + " in the hat";
于 2008-08-09T19:51:24.607 回答
0

这真的取决于。对于连接很少的小字符串,实际上仅附加字符串会更快。

String s = "String A" + "String B";

但是对于较大的字符串(非常非常大的字符串),使用 StringBuilder 会更有效。

于 2008-08-09T14:31:51.117 回答
0

在上述两种情况下,我都想将一个或多个字符串注入到预定义模板字符串的中间。

在这种情况下,我建议 String.Format 是最快的,因为它是专门为这个目的而设计的。

于 2008-08-09T16:29:55.513 回答
0

这实际上取决于您的使用模式。和之间的
详细基准可以在此处找到:String.Format 不适合密集型日志记录string.Joinstring,Concatstring.Format

于 2010-08-01T18:40:40.097 回答
-1

我建议不要,因为 String.Format 不是为连接而设计的,它是为格式化各种输入(如日期)的输出而设计的。

String s = String.Format("Today is {0:dd-MMM-yyyy}.", DateTime.Today);
于 2008-08-09T14:28:47.550 回答