When is it better to use String.Format vs string concatenation?

I've got a small piece of code that is parsing an index value to determine a cell input into Excel. It's got me thinking...

What's the difference between

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

and

xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);

Is one "better" than the other? And why?

99480 次浏览

在 C # 6之前

老实说,我认为第一个版本更简单——尽管我会简化为:

xlsSheet.Write("C" + rowIndex, null, title);

I suspect other answers talk about the performance hit, but to be honest it'll be minimal if present at all - and this concatenation version doesn't need to parse the format string.

格式字符串用于本地化等目的非常好,但在这种情况下,连接更简单,工作也一样。

用 C # 6

在 C # 6中,字符串插值使得很多东西更容易阅读。在这种情况下,您的第二段代码变成:

xlsSheet.Write($"C{rowIndex}", null, title);

which is probably the best option, IMO.

如果你的字符串比较复杂,有很多变量串联在一起,那么我会选择字符串。格式()。但是对于字符串的大小和连接的变量的数量,我会选择你的第一个版本,它更多的是 斯巴达人

这个例子可能太琐碎了,以至于没有注意到区别。事实上,我认为在大多数情况下,编译器可以优化掉任何差异。

然而,如果我必须猜测,我会给 string.Format()一个更复杂的情况下的优势。但这更多的是一种直觉,它可能会更好地利用缓冲区,而不是产生多个不可变的字符串,而不是基于任何真实的数据。

我喜欢 String。格式化,因为它可以让你的格式化文本比内联连接更容易理解和阅读,也更加灵活,允许你格式化你的参数,但是对于像你这样的短期使用,我认为连接没有任何问题。

对于循环内部或大字符串中的连接,应该始终尝试使用 StringBuilder 类。

字符串连接比字符串占用更多的内存。格式。因此串联字符串的最佳方法是使用 String。格式或系统。短信。StringBuilder 对象。

Let's take first case: "C" + rowIndex.ToString() 让我们假设 rowIndex 是一个值类型,因此 ToString ()方法必须将值转换为 String,然后 CLR 为包含这两个值的新字符串创建内存。

就像绳子一样。Format 期望对象参数,并将 rowIndex 作为一个对象,在内部将其转换为字符串,当然会有 Boxing,但它是内部的,而且它不会像第一种情况那样占用那么多内存。

对于短弦来说,我想这没什么大不了的。

我认为第一个选项更具可读性,这应该是你首要关心的问题。

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

绳子。Format 在底层使用了 StringBuilder (请查看 反射器) ,因此除非进行大量的连接,否则它不会带来任何性能优势。这对于你的场景来说会慢一些,但是现实是这种微观性能优化决策在大多数情况下是不合适的,你应该真正关注代码的可读性,除非你在一个循环中。

无论哪种方式,首先编写可读性,然后如果您确实认为存在性能问题,则使用 性能分析器性能分析器来识别热点。

对于一个简单的单一连接的简单案例,我觉得不值得使用 string.Format的复杂性(我还没有测试过,但是我怀疑对于像这样的简单案例,string.Format 也许吧稍微慢一点,因为有格式字符串解析等等)。像 Jon Skeet 一样,我不喜欢显式地调用 .ToString(),因为这将由 string.Concat(string, object)重载隐式地完成,而且我认为没有它,代码看起来更干净,更容易阅读。

但是对于多于几个连接(多少是主观的) ,我肯定更喜欢 string.Format。在某种程度上,我认为可读性和性能都不必要地受到连接的影响。

If there are many parameters to the format string (again, "many" is subjective), I usually prefer to include commented indices on the replacement arguments, lest I lose track of which value goes to which parameter. A contrived example:

Console.WriteLine(
"Dear {0} {1},\n\n" +


"Our records indicate that your {2}, \"{3}\", is due for {4} {5} shots.\n" +
"Please call our office at 1-900-382-5633 to make an appointment.\n\n" +


"Thank you,\n" +
"Eastern Veterinary",


/*0*/client.Title,
/*1*/client.LastName,
/*2*/client.Pet.Animal,
/*3*/client.Pet.Name,
/*4*/client.Pet.Gender == Gender.Male ? "his" : "her",
/*5*/client.Pet.Schedule[0]
);

更新

我突然想到,我给出的示例有点令人困惑,因为我似乎在这里使用了 都有级联和 string.Format。是的,从逻辑和词汇上来说,我就是这么做的。但是所有的连接都会被编译器 1优化掉,因为它们都是字符串文字。所以在运行时,只有一个字符串。所以我想我应该说,我更喜欢避免许多串联 at run time

当然,这个主题的大部分内容现在已经过时了,除非您仍然坚持使用 C # 5或更老的版本。现在我们有了 interpolated strings,它的可读性远远优于 string.Format,在几乎所有的情况下。现在,除非我只是将一个值直接连接到字符串文字的开头或结尾,否则我几乎总是使用字符串插值。今天,我会这样写我之前的例子:

Console.WriteLine(
$"Dear {client.Title} {client.LastName},\n\n" +


$"Our records indicate that your {client.Pet.Animal}, \"{client.Pet.Name}\", " +
$"is due for {(client.Pet.Gender == Gender.Male ? "his" : "her")} " +
$"{client.Pet.Schedule[0]} shots.\n" +
"Please call our office at 1-900-382-5633 to make an appointment.\n\n" +


"Thank you,\n" +
"Eastern Veterinary"
);

这样就会丢失编译时连接。每个插入的字符串都会被编译器转换成对 string.Format的调用,它们的结果会在运行时连接起来。这意味着这是为了可读性而牺牲运行时性能。大多数时候,这是一个值得的牺牲,因为运行时的损失是可以忽略不计的。但是,在性能关键代码中,您可能需要分析不同的解决方案。


1 您可以在 the C# specification:

中看到这一点

以下结构可以用常数表达式表示:

...

  • The predefined + ... binary operator...

You can also verify it with a little code:

const string s =
"This compiles successfully, " +
"and you can see that it will " +
"all be one string (named `s`) " +
"at run time";

我看了一下 String。格式(使用反射器) ,并且它实际上创建了一个 StringBuilder,然后在其上调用 AppendFormat。因此,它比连接的多次搅拌更快。最快的方法(我相信)是创建一个 StringBuilder 并手动调用 Append。当然,“许多”的数量是可以猜测的。 我会使用 + (实际上 & 因为我是一个 VB 程序员大部分)的东西像你的例子。当它变得更复杂时,我使用 String。格式。如果有很多变量,那么我会使用 StringBuilder 和 Append,例如,我们有构建代码的代码,在那里我使用一行实际的代码来输出一行生成的代码。

似乎有一些关于为每个操作创建多少字符串的推测,所以让我们举几个简单的例子。

"C" + rowIndex.ToString();

“ C”已经是一个字符串了。
rowIndex.ToString() creates another string. (@manohard - no boxing of rowIndex will occur)
然后我们得到最后一根弦。
If we take the example of

String.Format("C(0)",rowIndex);

then we have "C{0}" as a string
将 rowIndex 装箱以传递给函数
创建了一个新的字符串构造器
在字符串构建器上调用 AppendFormat ——我不知道 AppendFormat 如何运行的细节,但是让我们假设它非常高效,它仍然需要将装箱的 rowIndex 转换为字符串。
然后将字符串构建器转换为新的字符串。
I know that StringBuilders attempt to prevent pointless memory copies from taking place but the String.Format still ends up with extra overhead compared to the plain concatenation.

如果我们现在举一个有更多字符串的例子

"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();

we have 6 strings to start with, which will be the same for all cases.
使用连接,我们还有4个中间字符串加上最终的结果。使用 String、 Format (或 StringBuilder)消除的是那些中间结果。
请记住,要创建每个中间字符串,必须将前一个中间字符串复制到新的内存位置,而不仅仅是内存分配可能比较慢。

绳子。当格式模板(“ C {0}”)存储在配置文件(比如 Web.config/App.config)中时,格式可能是更好的选择

I did a bit of profiling of various string methods including string.Format, StringBuilder and string concatenation. String concatenation almost always outperformed the other methods of building strings. So, if performance is key, then its better. However, if performance is not critical then I personally find string.Format to be easier to follow in code. (But that's a subjective reason) StringBuilder however, is probably most efficient with respect to memory utilization.

我最初的偏好(来自 C + + 背景)是 String。格式。我后来放弃了这个,原因如下:

  • 字符串连接可以说是“更安全”。在我身上发生过(在其他几个开发人员身上也发生过)删除一个参数,或者错误地弄乱参数顺序。编译器不会根据格式字符串检查参数,最终会出现一个运行时错误(也就是说,如果你足够幸运,没有使用一个模糊的方法,比如记录一个错误)。通过连接,删除参数不易出错。你可以说出错的几率非常小,但是却发生了

字符串串联允许空值,而 String.Format不允许。写入“ s1 + null + s2”不会中断,它只是将空值视为 String。空的。嗯,这可能取决于您的特定场景-有些情况下,您希望出现一个错误,而不是默默地忽略空 FirstName。然而,即使在这种情况下,我个人也更喜欢自己检查 null 并抛出特定的错误,而不是我从 String 获得的标准 ArgumentNullException。格式。

  • 字符串连接性能更好。上面的一些帖子已经提到了这一点(但没有解释原因,所以我决定写这篇文章:)。

想法是.NET 编译器足够聪明,可以转换这段代码:

public static string Test(string s1, int i2, int i3, int i4,
string s5, string s6, float f7, float f8)
{
return s1 + " " + i2 + i3 + i4 + " ddd " + s5 + s6 + f7 + f8;
}

回到这里:

public static string Test(string s1, int i2, int i3, int i4,
string s5, string s6, float f7, float f8)
{
return string.Concat(new object[] { s1, " ", i2, i3, i4,
" ddd ", s5, s6, f7, f8 });
}

在 String 的引擎盖下发生了什么。连接很容易猜测(使用反射器)。数组中的对象通过 ToString ()转换为它们的字符串。然后计算总长度,只分配一个字符串(使用总长度)。最后,在某段不安全的代码中,通过 wstrcpy 将每个字符串复制到结果字符串中。

为什么 String.Concat更快?好吧,我们都可以看看 String.Format在做什么-你会对处理格式字符串所需的代码量感到惊讶。除此之外(我已经看到关于内存消耗的注释) ,String.Format在内部使用 StringBuilder。方法如下:

StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));

So for every passed argument, it reserves 8 characters. If the argument is a one-digit value, then too bad, we have some wasted space. If the argument is a custom object returning some long text on ToString(), then there might be even some reallocation needed (worst-case scenario, of course).

与此相比,串联只会浪费对象数组的空间(考虑到它是一个引用数组,不会浪费太多)。没有对格式说明符的解析,也没有中间的 StringBuilder。两种方法都存在装箱/取消装箱开销。

我选 String 的唯一原因。格式是当涉及到本地化时。将格式字符串放在资源中可以支持不同的语言,而不会影响代码(考虑一下格式化值根据语言改变顺序的情况,即“在{0}小时和{1}分钟之后”在日语中可能看起来非常不同:)。


总结一下我的第一篇(也是相当长的)文章:

  • 对我来说,最好的方法(在性能与可维护性/可读性方面)是使用字符串串联,而不需要任何 ToString()调用
  • 如果你是在性能之后,使 ToString()调用自己,以避免装箱(我有点偏向于可读性)-同样的第一个选项在您的问题
  • 如果向用户显示本地化字符串(这里不是这种情况) ,String.Format()有一个边。

关于性能,我更喜欢 String. Format

我同意上面的很多观点,我认为应该提到的另一点是代码的可维护性。绳子。格式允许更容易地更改代码。

也就是说,我有一个信息 "The user is not authorized for location " + location"The User is not authorized for location {0}"

if I ever wanted to change the message to say: location + " does not allow this User Access" or "{0} does not allow this User Access"

格式化我所要做的就是改变字符串。 为了连接,我必须修改那条消息

如果在多个地方使用,可以节省很多时间。

在我的印象中,string.format 速度更快,在这个测试中似乎慢了3倍

string concat = "";
System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch    ();
sw1.Start();
for (int i = 0; i < 10000000; i++)
{
concat = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}","1", "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" , i);
}
sw1.Stop();
Response.Write("format: "  + sw1.ElapsedMilliseconds.ToString());
System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
sw2.Start();
for (int i = 0; i < 10000000; i++)
{
concat = "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10" + i;
}
sw2.Stop();

Format 需要4.6秒,使用’+’需要1.6秒。

关于表现:

void Main()
{
var start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = "Hi " + i.ToString() + "; Hi to you " + (i * 2).ToString();
}
var end = CurrentTimeMillis();
Console.WriteLine("Concatenation = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = String.Format("Hi {0}; Hi to you {1}", i, +i * 2);
}
end = CurrentTimeMillis();
Console.WriteLine("Format = " + ((end - start)).ToString() + " millisecond");
start = CurrentTimeMillis();
for (var i = 0; i < 1000000; i++)
{
var s = String.Concat("Hi ", i.ToString(), "; Hi to you ", (i * 2).ToString());
}
end = CurrentTimeMillis();
Console.WriteLine("Strng.concat = " + ((end - start)).ToString() + " millisecond");


start = CurrentTimeMillis();
for (int i = 0; i < 1000000; i++)
{
StringBuilder bldString = new StringBuilder("Hi ");
bldString.Append(i).Append("; Hi to you ").Append(i * 2);
}
end = CurrentTimeMillis();
Console.WriteLine("String Builder = " + ((end - start)) + " millisecond");
}


private static readonly DateTime Jan1st1970 = new DateTime
(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);


public static long CurrentTimeMillis()
{
return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds;
}

结果

Concatenation = 69 millisecond
Format = 142 millisecond
Strng.concat = 62 millisecond
String Builder = 91 millisecond

为了进行公平的比较,我实例化了一个新的 StringBuilder,而不是在循环外创建的 StringBuilder (由于循环附加在一个构建器的重新分配空间的末尾,这样可能更快)。