字符串输出:格式或concat在C#?

假设您要输出或连接字符串。 您更喜欢以下哪种款式?

  • var p = new { FirstName = "Bill", LastName = "Gates" };

  • Console.WriteLine("{0} {1}", p.FirstName, p.LastName);

  • Console.WriteLine(p.FirstName + " " + p.LastName);

你宁愿使用格式还是只是简单地串接字符串? 什么是你最喜欢的? 这其中的一个伤害你的眼睛?

你有任何理性的论点来使用一个而不是另一个?

我会去第二个

试试这个代码。

这是你的代码稍微修改的版本。
1.我删除了Console.WriteLine,因为它可能比我要测量的要慢几个数量级。
2.我在循环之前启动Stopwatch,并在之后立即停止,这样我不会失去精度,如果函数需要例如26.4滴答执行。
来自“简明英汉词典”用迭代法分解结果的方式是错误的。 看看如果你有1000毫秒和100毫秒会发生什么。 在这两种情况下,你将得到0毫秒,除以1000000。

 Stopwatch s = new Stopwatch(); var p = new { FirstName = "Bill", LastName = "Gates" }; int n = 1000000; long fElapsedMilliseconds = 0, fElapsedTicks = 0, cElapsedMilliseconds = 0, cElapsedTicks = 0; string result; s.Start(); for (var i = 0; i < n; i++) result = (p.FirstName + " " + p.LastName); s.Stop(); cElapsedMilliseconds = s.ElapsedMilliseconds; cElapsedTicks = s.ElapsedTicks; s.Reset(); s.Start(); for (var i = 0; i < n; i++) result = string.Format("{0} {1}", p.FirstName, p.LastName); s.Stop(); fElapsedMilliseconds = s.ElapsedMilliseconds; fElapsedTicks = s.ElapsedTicks; s.Reset(); Console.Clear(); Console.WriteLine(n.ToString()+" x result = string.Format(\"{0} {1}\", p.FirstName, p.LastName); took: " + (fElapsedMilliseconds) + "ms - " + (fElapsedTicks) + " ticks"); Console.WriteLine(n.ToString() + " x result = (p.FirstName + \" \" + p.LastName); took: " + (cElapsedMilliseconds) + "ms - " + (cElapsedTicks) + " ticks"); Thread.Sleep(4000); 

这些是我的结果:

1000000 x result = string.Format(“{0} {1}”,p.FirstName,p.LastName); 采取:618ms – 2213706蜱
1000000 x result =(p.FirstName +“”+ p.LastName); 采取:166ms – 595610蜱

我很惊讶,很多人立刻想要找到执行速度最快的代码。 如果一百万次迭代仍然需要不到一秒钟的时间来处理,这是否会以最终用户注意的方式出现? 不太可能。

过早优化=失败。

我会去与String.Format选项,只是因为它从建筑的角度来看是最有意义的。 我不关心性能,直到成为一个问题(如果是这样的话,我会问自己:我是否需要一次连接上百万个名字?当然,他们不会全都适合在屏幕上…)

考虑您的客户是否稍后要更改它,以便他们可以配置是否显示"Firstname Lastname""Lastname, Firstname." 使用格式选项,这很容易 – 只需换出格式字符串。 用concat,你需要额外的代码。 当然,在这个特定的例子中,这听起来不是什么大不了的事情,而是推断出来的。

哦,亲爱的 – 在阅读了其他回复之一之后,我试着颠倒了操作的顺序 – 所以首先执行连接,然后是String.Format …

 Bill Gates Console.WriteLine(p.FirstName + " " + p.LastName); took: 8ms - 30488 ticks Bill Gates Console.WriteLine("{0} {1}", p.FirstName, p.LastName); took: 0ms - 182 ticks 

所以操作的顺序会有很大的差别,或者说第一个操作总是慢得多。

这里是运行结果不止一次的运行结果。 我试图改变订单,但是一旦第一个结果被忽略,事情通常遵循相同的规则:

 Bill Gates Console.WriteLine(FirstName + " " + LastName); took: 5ms - 20335 ticks Bill Gates Console.WriteLine(FirstName + " " + LastName); took: 0ms - 156 ticks Bill Gates Console.WriteLine(FirstName + " " + LastName); took: 0ms - 122 ticks Bill Gates Console.WriteLine("{0} {1}", FirstName, LastName); took: 0ms - 181 ticks Bill Gates Console.WriteLine("{0} {1}", FirstName, LastName); took: 0ms - 122 ticks Bill Gates String.Concat(FirstName, " ", LastName); took: 0ms - 142 ticks Bill Gates String.Concat(FirstName, " ", LastName); took: 0ms - 117 ticks 

正如你可以看到后续运行相同的方法(我把代码重构成3个方法)递增得更快。 最快的似乎是Console.WriteLine(String.Concat(…))方法,接着是正常的串联,然后是格式化的操作。

最初的启动延迟可能是控制台流的初始化,因为在第一次操作之前放置一个Console.Writeline(“Start!”)会使所有时间恢复正常。

字符串是不可改变的,这意味着在代码中反复使用相同的小部分内存。 将相同的两个字符串添加在一起并反复创建相同的新字符串不会影响内存。 .Net足够聪明,只是使用相同的内存引用。 因此你的代码并不真正测试两个concat方法之间的区别。

试试这个大小:

 Stopwatch s = new Stopwatch(); int n = 1000000; long fElapsedMilliseconds = 0, fElapsedTicks = 0, cElapsedMilliseconds = 0, cElapsedTicks = 0, sbElapsedMilliseconds = 0, sbElapsedTicks = 0; Random random = new Random(DateTime.Now.Millisecond); string result; s.Start(); for (var i = 0; i < n; i++) result = (random.Next().ToString() + " " + random.Next().ToString()); s.Stop(); cElapsedMilliseconds = s.ElapsedMilliseconds; cElapsedTicks = s.ElapsedTicks; s.Reset(); s.Start(); for (var i = 0; i < n; i++) result = string.Format("{0} {1}", random.Next().ToString(), random.Next().ToString()); s.Stop(); fElapsedMilliseconds = s.ElapsedMilliseconds; fElapsedTicks = s.ElapsedTicks; s.Reset(); StringBuilder sb = new StringBuilder(); s.Start(); for(var i = 0; i < n; i++){ sb.Clear(); sb.Append(random.Next().ToString()); sb.Append(" "); sb.Append(random.Next().ToString()); result = sb.ToString(); } s.Stop(); sbElapsedMilliseconds = s.ElapsedMilliseconds; sbElapsedTicks = s.ElapsedTicks; s.Reset(); Console.WriteLine(n.ToString() + " x result = string.Format(\"{0} {1}\", p.FirstName, p.LastName); took: " + (fElapsedMilliseconds) + "ms - " + (fElapsedTicks) + " ticks"); Console.WriteLine(n.ToString() + " x result = (p.FirstName + \" \" + p.LastName); took: " + (cElapsedMilliseconds) + "ms - " + (cElapsedTicks) + " ticks"); Console.WriteLine(n.ToString() + " x sb.Clear();sb.Append(random.Next().ToString()); sb.Append(\" \"); sb.Append(random.Next().ToString()); result = sb.ToString(); took: " + (sbElapsedMilliseconds) + "ms - " + (sbElapsedTicks) + " ticks"); Console.WriteLine("****************"); Console.WriteLine("Press Enter to Quit"); Console.ReadLine(); 

示例输出:

 1000000 x result = string.Format("{0} {1}", p.FirstName, p.LastName); took: 513ms - 1499816 ticks 1000000 x result = (p.FirstName + " " + p.LastName); took: 393ms - 1150148 ticks 1000000 x sb.Clear();sb.Append(random.Next().ToString()); sb.Append(" "); sb.Append(random.Next().ToString()); result = sb.ToString(); took: 405ms - 1185816 ticks 

可怜的翻译

如果你知道你的申请会保持英文,那么很好,保存时钟滴答。 然而,许多文化通常会看到姓氏名,例如地址。

所以使用string.Format() ,特别是如果你string.Format()你的应用程序去任何地方英语不是第一语言。

这是我的结果超过100,000次迭代:

 Console.WriteLine("{0} {1}", p.FirstName, p.LastName); took (avg): 0ms - 689 ticks Console.WriteLine(p.FirstName + " " + p.LastName); took (avg): 0ms - 683 ticks 

这里是板凳代码:

 Stopwatch s = new Stopwatch(); var p = new { FirstName = "Bill", LastName = "Gates" }; //First print to remove the initial cost Console.WriteLine(p.FirstName + " " + p.LastName); Console.WriteLine("{0} {1}", p.FirstName, p.LastName); int n = 100000; long fElapsedMilliseconds = 0, fElapsedTicks = 0, cElapsedMilliseconds = 0, cElapsedTicks = 0; for (var i = 0; i < n; i++) { s.Start(); Console.WriteLine(p.FirstName + " " + p.LastName); s.Stop(); cElapsedMilliseconds += s.ElapsedMilliseconds; cElapsedTicks += s.ElapsedTicks; s.Reset(); s.Start(); Console.WriteLine("{0} {1}", p.FirstName, p.LastName); s.Stop(); fElapsedMilliseconds += s.ElapsedMilliseconds; fElapsedTicks += s.ElapsedTicks; s.Reset(); } Console.Clear(); Console.WriteLine("Console.WriteLine(\"{0} {1}\", p.FirstName, p.LastName); took (avg): " + (fElapsedMilliseconds / n) + "ms - " + (fElapsedTicks / n) + " ticks"); Console.WriteLine("Console.WriteLine(p.FirstName + \" \" + p.LastName); took (avg): " + (cElapsedMilliseconds / n) + "ms - " + (cElapsedTicks / n) + " ticks"); 

所以,我不知道谁的答复标记为答案:)

在一个简单的场景中连接字符串是很好的 – 比任何更复杂的事情更复杂,甚至是LastName,FirstName。 通过这种格式,您可以一目了然地看到,当读取代码时,字符串的最终结构是什么,连接起来几乎不可能马上识别出最终的结果(除了像这样一个非常简单的例子)。

这意味着从长远来看,当你回来改变你的字符串格式时,你将能够弹出并对格式字符串进行一些调整,或者皱起你的额头并开始移动各种各样的财产访问者混合在一起,更容易引入问题。

如果你正在使用.NET 3.5,你可以使用像这样的扩展方法,并获得一个简单的流动,关闭这样的袖口语法:

 string str = "{0} {1} is my friend. {3}, {2} is my boss.".FormatWith(prop1,prop2,prop3,prop4); 

最后,随着应用程序复杂度的增长,您可能会决定在应用程序中合理维护字符串,以便将它们移动到资源文件中以进行本地化或简单地转换为静态帮助程序。 如果你一直使用格式,这将是更容易实现,你的代码可以很简单地重构使用类似的东西

 string name = String.Format(ApplicationStrings.General.InformalUserNameFormat,this.FirstName,this.LastName); 

对于非常简单的操作,我会使用串联,但是一旦超过2或3个元素,格式将变得更加适合IMO。

另一个更喜欢String.Format的原因是.NET字符串是不可变的,这样做创建更少的临时/中间副本。

虽然我完全理解了风格偏好,并且在部分基于自己的偏好的情况下为我的第一个答案选择了连接,但是我的决定的一部分是基于连接会更快的想法。 所以,出于好奇,我测试了它,结果是惊人的,特别是对于这样一个小字符串。

使用下面的代码:

  System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch(); var p = new { FirstName = "Bill", LastName = "Gates" }; s.Start(); Console.WriteLine("{0} {1}", p.FirstName, p.LastName); s.Stop(); Console.WriteLine("Console.WriteLine(\"{0} {1}\", p.FirstName, p.LastName); took: " + s.ElapsedMilliseconds + "ms - " + s.ElapsedTicks + " ticks"); s.Reset(); s.Start(); Console.WriteLine(p.FirstName + " " + p.LastName); s.Stop(); Console.WriteLine("Console.WriteLine(p.FirstName + \" \" + p.LastName); took: " + s.ElapsedMilliseconds + "ms - " + s.ElapsedTicks + " ticks"); 

我得到了以下结果:

 Bill Gates Console.WriteLine("{0} {1}", p.FirstName, p.LastName); took: 2ms - 7280 ticks Bill Gates Console.WriteLine(p.FirstName + " " + p.LastName); took: 0ms - 67 ticks 

使用格式化方法超过100倍! 连接甚至没有注册为1ms,这就是为什么我输出计时器滴答。

对于基本的字符串连接,我通常使用第二种风格 – 更容易阅读和更简单。 但是,如果我正在做一个更复杂的字符串组合,我通常选择String.Format。

String.Format节省大量的引号和加号…

 Console.WriteLine("User {0} accessed {1} on {2}.", user.Name, fileName, timestamp); vs Console.WriteLine("User " + user.Name + " accessed " + fileName + " on " + timestamp + "."); 

只有少数人保存了,但我认为,在这个例子中,格式更清洁。

更好的测试是使用Perfmon和CLR内存计数器来观察你的内存。 我的理解是,你想要使用String.Format而不是仅仅连接字符串的全部原因是,因为字符串是不可变的,所以你不必要地用临时字符串加载垃圾收集器,这需要在下一次回收。

StringBuilder和String.Format尽管可能较慢,但是内存效率更高。

关于字符串连接有什么不好?

一般来说,我更喜欢前者,特别是当字符串变长时,阅读起来更容易。

另一个好处是我相信性能,因为后者实际上执行2个字符串创建语句,然后将最终字符串传递给Console.Write方法。 String.Format使用一个StringBuilder,我相信,所以可以避免多个连接。

但是应该注意的是,如果传递给String.Format的参数(和Console.Write等其他类似的方法)是值类型,那么它们在传入之前将被装箱,这可以提供它自己的性能命中。 这里的博客文章 。

从C#6.0 插值字符串开始可以用来做到这一点,这更加简化了格式。

 var name = "Bill"; var surname = "Gates"; MessageBox.Show($"Welcome to the show, {name} {surname}!"); 

插入的字符串表达式看起来像包含表达式的模板字符串。 插入的字符串表达式通过用表达式结果的ToString表示替换包含的表达式来创建字符串。

插值字符串与String.Format具有相似的性能,但由于值和表达式插入在内,因此可提高可读性和更短的语法。

请参考这个关于字符串插值的dotnetperls文章 。

如果您正在寻找一种默认的格式化字符串的方法,那么这在可读性和性能方面是有意义的(除非微秒会在您的特定用例中发挥作用)。

  1. 格式化是“.NET”的方式。 某些重构工具(Refactor!for one)甚至会建议重构连续风格的代码以使用格式化风格。
  2. 格式化对于编译器来说更容易优化(尽管第二种可能会被重构为使用“Concat”方法)。
  3. 格式化通常更清晰(特别是“花哨”的格式)。
  4. 格式化意味着对所有变量隐式调用“.ToString”,这对于可读性是有利的。
  5. 根据“有效的C#”,.NET的“WriteLine”和“格式”实现混乱,他们autobox所有值类型(这是不好的)。 “有效的C#”建议明确执行“.ToString”调用,这是恕我直言,假的(见杰夫张贴 )
  6. 此刻,编译器不检查格式化类型提示,导致运行时错误。 但是,这可以在将来的版本中修改。

我根据可读性来选择。 当变量周围有一些文本时,我更喜欢格式选项。 在这个例子中:

 Console.WriteLine("User {0} accessed {1} on {2}.", user.Name, fileName, timestamp); 

即使没有变数的名字,你也能理解这个意思,而concat则是混杂着引号和+符号,迷惑了我的眼睛:

 Console.WriteLine("User " + user.Name + " accessed " + fileName + " on " + timestamp + "."); 

(我借用了Mike的榜样,因为我喜欢它)

如果格式化字符串没有变量名称,那么我必须使用concat:

  Console.WriteLine("{0} {1}", p.FirstName, p.LastName); 

格式选项使我读取变量名称并将其映射到相应的数字。 concat选项不需要。 我仍然对引号和+符号感到困惑,但是替代方案更糟。 红宝石?

  Console.WriteLine(p.FirstName + " " + p.LastName); 

性能方面,我期望format选项比concat慢,因为format需要解析字符串。 我不记得要优化这种类型的指令,但是如果我这样做,我会看看像Concat()Join()这样的string方法。

格式的另一个优点是格式字符串可以放在配置文件中。 非常方便与错误消息和UI文本。

如果您打算本地化结果,那么String.Format是必不可少的,因为不同的自然语言甚至可能不具有相同顺序的数据。

我会使用String.Format,但是我也会在资源文件中使用格式化字符串,以便可以对其他语言进行本地化。 使用简单的字符串concat不允许你这样做。 显然,如果你不需要本地化字符串,这不是一个考虑的理由。 这真的取决于字符串是什么。

如果将显示给用户,我会使用String.Format,所以我可以本地化,如果我需要 – 而FxCop会拼写检查它,以防万一:)

如果它包含数字或任何其他非字符串的东西(例如日期),我会使用String.Format,因为它使我能够更好地控制格式 。

如果是建立一个SQL查询,我会使用Linq 。

如果要在一个循环内连接字符串,我会使用StringBuilder来避免性能问题。

如果这是用户不会看到的一些输出,而不会影响性能,我会使用String.Format,因为我习惯于使用它,我只是习惯它:)

从现在开始的一个星期,2015年8月19日,这个问题将是七(7)岁。 现在有一个更好的方法来做到这一点。 在可维护性方面更好 ,因为我没有进行任何性能测试,而只是串联字符串(但是现在呢?几毫秒的差距呢?)。 用C#6.0做的新方法:

 var p = new { FirstName = "Bill", LastName = "Gates" }; var fullname = $"{p.FirstName} {p.LastName}"; 

这个新功能更好 ,海事组织, 在我们的情况下实际上更好,因为我们有代码,我们建立querystrings的价值取决于一些因素。 设想一个查询字符串,我们有6个参数。 所以,而不是做一个,例如:

 var qs = string.Format("q1={0}&q2={1}&q3={2}&q4={3}&q5={4}&q6={5}", someVar, anotherVarWithLongName, var3, var4, var5, var6) 

在这里可以这样写,更容易阅读:

 var qs=$"q1={someVar}&q2={anotherVarWithLongName}&q3={var3}&q4={var4}&q5={var5}&q6={var6}"; 

如果你正在处理的东西需要易于阅读(而这是大多数代码),我会坚持运营商超载版本,除非:

  • 代码需要执行数百万次
  • 你正在做吨连接(超过4是一吨)
  • 代码是针对精简框架

至少在这两种情况下,我会使用StringBuilder来代替。

我认为这在很大程度上取决于产出的复杂程度。 我倾向于选择当时最适合的方案。

根据工作选择合适的工具:D无论哪一个看起来最干净!

我更喜欢第二种,但是目前我还没有理由支持这一立场。

好一个!

刚刚添加

  s.Start(); for (var i = 0; i < n; i++) result = string.Concat(p.FirstName, " ", p.LastName); s.Stop(); ceElapsedMilliseconds = s.ElapsedMilliseconds; ceElapsedTicks = s.ElapsedTicks; s.Reset(); 

它甚至更快(我猜string.Concat在这两个例子中被调用,但是第一个需要某种翻译)。

 1000000 x result = string.Format("{0} {1}", p.FirstName, p.LastName); took: 249ms - 3571621 ticks 1000000 x result = (p.FirstName + " " + p.LastName); took: 65ms - 944948 ticks 1000000 x result = string.Concat(p.FirstName, " ", p.LastName); took: 54ms - 780524 ticks 

既然我不认为这里的答案涵盖了一切,我想在这里做一个小小的补充。

Console.WriteLine(string format, params object[] pars)调用string.Format 。 “+”意味着字符串连接。 我不认为这总是与风格有关; 我倾向于混合两种风格,这取决于我所处的环境。

简短的回答

您正在面临的决定与字符串分配有关。 我会尽量简化。

说你有

 string s = a + "foo" + b; 

如果您执行此操作,它将评估如下:

 string tmp1 = a; string tmp2 = "foo" string tmp3 = concat(tmp1, tmp2); string tmp4 = b; string s = concat(tmp3, tmp4); 

这里的tmp并不是一个真正的局部变量,但是它是JIT的一个临时变量(它被推入IL栈)。 如果你在栈上压入一个字符串(比如IL中的ldstr for literals),你可以在栈上放一个字符串指针的引用。

当你调用concat这个引用就成了一个问题,因为没有任何可用的字符串参考包含这两个字符串。 这意味着.NET需要分配一个新的内存块,然后用两个字符串填充它。 这是一个问题,原因是分配相对昂贵。

这将问题改变为:如何减少concat操作的数量?

所以,最简单的答案是: string.Format > 1 concats,“+”对于1 concat可以正常工作。 如果你不关心如何进行微型性能优化,那么在一般情况下, string.Format就可以正常工作。

关于文化的说明

然后有一种叫文化的东西

string.Format使您能够在格式化中使用CultureInfo 。 简单的操作符“+”使用当前的文化。

如果您正在编写文件格式和f.ex,这尤其是一个重要的评论。 将“添加”到字符串的double值。 在不同的机器上,如果你没有使用带有显式CultureInfo string.Format ,你可能会得到不同的字符串。

F.ex. 考虑如果你改变一个'。'会发生什么。 对于','写逗号分隔值文件…在荷兰语中的小数点分隔符是一个逗号,所以你的用户可能会得到一个'有趣'的惊喜。

更详细的答案

如果事先不知道字符串的确切大小,最好使用这样的策略来重新定位所使用的缓冲区。 首先填充松弛空间,然后复制数据。

增长意味着分配一个新的内存块并将旧的数据复制到新的缓冲区。 旧的内存块可以被释放。 在这一点上你得到了底线:成长是一个昂贵的操作。

最切实可行的方法是使用分配政策。 最常见的策略是用2的权力来重新设置缓冲区。当然,你必须做得比这个更聪明一些(因为如果你已经知道你需要128个字符,从1,2,4,8增长就没有意义了),但你得到的照片。 这项政策确保您不需要太多的上述昂贵的操作。

StringBuilder是一个类,基本上重新分配两个幂的底层缓冲区。 string.Format使用下面的StringBuilder

这使得你的决定是一个基本的权衡和追加(多重)(w / wo文化)或者只是分配和追加之间的权衡。

就个人而言,第二个是你所使用的所有东西都是按照它的输出顺序排列的。而第一个你必须把{0}和{1}与适当的var匹配起来,这很容易搞乱。

至少它不像C ++ sprintf那样糟糕,如果你得到的变量类型错了,整个事情就会炸毁。

Also, since the second is all inline and it doesn't have to do any searching and replacing for all the {0} things, the latter should be faster… though I don't know for sure.

I actually like the first one because when there are a lot of variables intermingled with the text it seems easier to read to me. Plus, it is easier to deal with quotes when using the string.Format(), uh, format. Here is decent analysis of string concatenation.

I've always gone the string.Format() route. Being able to store formats in variables like Nathan's example is a great advantage. In some cases I may append a variable but once more than 1 variable is being concatenated I refactor to use formatting.

Oh, and just for completeness, the following is a few ticks faster than normal concatenation:

 Console.WriteLine(String.Concat(p.FirstName," ",p.LastName)); 

The first one (format) looks better to me. It's more readable and you are not creating extra temporary string objects.

I was curious where StringBuilder stood with these tests. Results below…

 class Program { static void Main(string[] args) { var p = new { FirstName = "Bill", LastName = "Gates" }; var tests = new[] { new { Name = "Concat", Action = new Action(delegate() { string x = p.FirstName + " " + p.LastName; }) }, new { Name = "Format", Action = new Action(delegate() { string x = string.Format("{0} {1}", p.FirstName, p.LastName); }) }, new { Name = "StringBuilder", Action = new Action(delegate() { StringBuilder sb = new StringBuilder(); sb.Append(p.FirstName); sb.Append(" "); sb.Append(p.LastName); string x = sb.ToString(); }) } }; var Watch = new Stopwatch(); foreach (var t in tests) { for (int i = 0; i < 5; i++) { Watch.Reset(); long Elapsed = ElapsedTicks(t.Action, Watch, 10000); Console.WriteLine(string.Format("{0}: {1} ticks", t.Name, Elapsed.ToString())); } } } public static long ElapsedTicks(Action ActionDelg, Stopwatch Watch, int Iterations) { Watch.Start(); for (int i = 0; i < Iterations; i++) { ActionDelg(); } Watch.Stop(); return Watch.ElapsedTicks / Iterations; } } 

结果:

Concat: 406 ticks
Concat: 356 ticks
Concat: 411 ticks
Concat: 299 ticks
Concat: 266 ticks
Format: 5269 ticks
Format: 954 ticks
Format: 1004 ticks
Format: 984 ticks
Format: 974 ticks
StringBuilder: 629 ticks
StringBuilder: 484 ticks
StringBuilder: 482 ticks
StringBuilder: 508 ticks
StringBuilder: 504 ticks

According to the MCSD prep material, Microsoft suggests using the + operator when dealing with a very small number of concatenations (probably 2 to 4). I'm still not sure why, but it's something to consider.