字符串输出:C# 中的 format 还是 concat?

String output: format or concat in C#?

提问人: 提问时间:8/19/2008 最后编辑:6 revs, 4 users 63%Philippe 更新时间:7/12/2021 访问量:100915

问:

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

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

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

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

您是宁愿使用格式还是简单地连接字符串?你最喜欢什么?其中之一会伤害你的眼睛吗?

你有什么合理的论据来使用一个而不是另一个吗?

我会选择第二个。

C# 字符串 编码样式 string.format

评论


答:

1赞 2 revs, 2 users 89%Adam Haile #1

就个人而言,第二个,因为您正在使用的所有内容都按直接顺序输出。而对于第一个,您必须将{0}和{1}与适当的 var 相匹配,这很容易搞砸。

至少它不像 C++ 冲刺那么糟糕,如果你弄错了变量类型,整个事情就会爆炸。

此外,由于第二个是全内联的,并且不必对所有{0}事物进行任何搜索和替换,因此后者应该更快......虽然我不确定。

2赞 mercutio #2

我认为这很大程度上取决于输出的复杂程度。我倾向于选择当时最有效的方案。

根据作业:D选择合适的工具以看起来最干净的为准!

7赞 user2189331 #3

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

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

2赞 user1921 #4

我也更喜欢第二种,但我目前没有理性的论据来支持这一立场。

5赞 3 revs, 2 users 83%samjudson #5

一般来说,我更喜欢前者,因为尤其是当字符串变长时,它会更容易阅读。

另一个好处是我相信其中一种性能,因为后者在将最终字符串传递给方法之前实际上执行了 2 个字符串创建语句。 我相信在幕后使用了 StringBuilder,因此避免了多个连接。Console.WriteString.Format

但是,应该注意的是,如果要传入的参数(以及 Console.Write 等其他此类方法)是值类型,则它们在传入之前将被装箱,这可以提供自己的性能影响。关于此的博客文章在这里String.Format

评论

1赞 Richard Slater 8/24/2009
那篇博文现在位于:jeffbarnes.net/blog/post/2006/08/08/...。我没有足够的代表来编辑。
5赞 Mike #6

对于基本的字符串连接,我通常使用第二种样式 - 更易于阅读和更简单。但是,如果我正在做一个更复杂的字符串组合,我通常会选择 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 + ".");

只有少数 charicter 保存了,但我认为,在这个例子中,格式使它更干净。

9赞 Nathan #7

在这样的简单场景中,连接字符串是可以的 - 它更复杂,任何比这更复杂的东西,甚至是 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);
1赞 adparadox #8

我实际上喜欢第一个,因为当文本中夹杂着很多变量时,对我来说似乎更容易阅读。此外,使用字符串时更容易处理引号。Format(),呃,格式。这是对字符串连接的体面分析

4赞 Konrad Rudolph #9
  1. 格式化是“.NET”的实现方式。某些重构工具(例如重构工具)甚至会建议重构 concat 样式代码以使用格式样式。
  2. 对于编译器来说,格式化更容易优化(尽管第二个可能会被重构为使用“Concat”方法,这种方法速度很快)。
  3. 格式通常更清晰易读(尤其是“花哨”格式)。
  4. 格式化意味着对 '.ToString“,这有利于提高可读性。
  5. 根据“有效的 C#”,.NET“WriteLine”和“Format”实现被搞砸了,它们会自动装箱所有值类型(这很糟糕)。“有效的 C#”建议执行 '.ToString“明确调用,恕我直言,这是假的(参见 Jeff 的帖子)
  6. 目前,编译器不会检查格式类型提示,从而导致运行时错误。但是,这可能会在将来的版本中进行修改。
6赞 Adam Haile #10

虽然我完全理解风格偏好,并为我的第一个答案选择了串联,部分基于我自己的偏好,但我的部分决定是基于串联会更快的想法。所以,出于好奇,我测试了它,结果令人震惊,尤其是对于这么小的字符串。

使用以下代码:

    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,这就是我也输出计时器滴答声的原因。

评论

2赞 erikkallen 12/1/2009
但是,当然,您应该多次执行该操作以获得测量值。
2赞 Aidanapword 12/17/2010
并丢失对 Console.Writeline() 的调用,因为它超出了问题的范围?
0赞 niico 10/27/2016
您是否使用字符串构建器进行过测试?;)
4赞 3 revs, 2 users 89%Wilka #11

我会使用 ,但我也会在资源文件中拥有格式字符串,以便它可以本地化为其他语言。使用简单的字符串 concat 不允许您这样做。显然,如果您永远不需要本地化该字符串,那么这不是考虑它的理由。这实际上取决于字符串的用途。String.Format

如果要向用户显示,我会使用它,这样我就可以在需要时进行本地化 - FxCop 会为我进行拼写检查,以防万一:)String.Format

如果它包含数字或任何其他非字符串内容(例如日期),我会使用,因为它让我可以更好地控制格式String.Format

如果是为了构建像 SQL 这样的查询,我会使用 Linq

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

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

1赞 Scott Muc #12

我总是走绳子。Format() 路由。能够像 Nathan 的例子一样将格式存储在变量中是一个很大的优势。在某些情况下,我可能会附加一个变量,但是一旦连接了 1 个以上的变量,我就会重构以使用格式。

1赞 samjudson #13

哦,为了完整起见,以下比正常串联快了几个刻度:

Console.WriteLine(String.Concat(p.FirstName," ",p.LastName));
55赞 4 revs, 2 users 98%samjudson #14

哦,亲爱的 - 在阅读了其他回复之一后,我尝试颠倒操作的顺序 - 所以首先执行连接,然后执行 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!”) 会使所有时间恢复正常。

评论

2赞 CShark 5/27/2016
然后从测试中完全删除 Console.WriteLine。它歪曲了结果!
0赞 drzaus 5/16/2020
出于这个确切的原因,在运行性能测试时,我总是从一次性或“控制”场景开始
0赞 Philippe #15

实际上,我昨天进行了这些测试,但天色已晚,所以我没有做出回应。

最重要的是,它们平均需要相同的时间。我做了超过 100000 次迭代的测试。

我也会尝试使用 StringBuilder,回家后我会发布代码和结果。

14赞 2 revs, 2 users 74%Philippe #16

以下是我在 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");

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

评论

0赞 user88637 6/7/2009
为什么这个答案的背景是蓝色的?
0赞 Davy8 7/5/2009
@yossi它是蓝色的,因为应答者与提问者相同
89赞 9 revs, 7 users 53%Michał Piaskowski #17

试试这段代码。

它是代码的略微修改版本。

  1. 我删除了 Console.WriteLine,因为它可能比我尝试测量的速度慢几个数量级。
  2. 我在循环之前启动秒表并在循环之后立即停止它,这样,如果函数需要 26.4 个刻度来执行,我就不会失去精度。
  3. 你用一些迭代来划分结果的方式是错误的。看看如果您有 1,000 毫秒和 100 毫秒会发生什么。在这两种情况下,除以 1,000,000 后将得到 0 毫秒。

法典:

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 结果 = 字符串。Format(“{0} {1}”, p.FirstName, p.LastName);采取: 618ms - 2213706 ticks
1000000 x result = (p.FirstName + “ ” + p.LastName);已:166 毫秒 - 595610 滴答

评论

1赞 Allon Guralnek 9/29/2011
很有意思。我的平均时间是 224 毫秒和 48 毫秒,提高了 x4.66,甚至比你的 x3.72 还要好。我想知道是否有一种编译后工具可以重写不使用任何复合格式化功能(即只是简单)的 IL,并用速度更快的字符串连接替换它们。我想知道这样的壮举可以通过现有的 IL 重写器(例如 PostSharp)来实现。string.Format{0}
33赞 Ludington 11/13/2012
字符串是不可变的,这意味着在代码中一遍又一遍地使用相同的小内存。将相同的两个字符串相加并一遍又一遍地创建相同的新字符串不会影响内存。.Net 足够聪明,只需使用相同的内存引用即可。因此,您的代码不会真正测试两种 concat 方法之间的差异。请参阅下面我回答中的代码。
1赞 puretppc 1/20/2014
老实说,我总是连接起来,因为它对我来说更容易阅读,而且哇,它更快:)
0赞 niico 10/27/2016
那么速度是选择其中之一的唯一原因吗?
2赞 Philippe #18

好东西!

刚刚添加

        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();

而且它甚至更快(我猜是字符串。在两个示例中都调用了 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

评论

2赞 Allon Guralnek 9/29/2011
由于编译器将基于运算符的字符串连接转换为对 的调用,因此所需的时间完全相同。它是在编译期间完成的,因此对运行时性能没有影响。如果多次运行测试或在较大的测试样本上运行测试,则会看到它们是相同的。string.Concat(...)
1赞 Rismo #19

第一个(格式)对我来说看起来更好。它更具可读性,并且您不会创建额外的临时字符串对象。

162赞 4 revs, 4 users 60%Fredrik Kalseth #20

令我惊讶的是,有这么多人立即想要找到执行速度最快的代码。如果 100 万次迭代仍然需要不到一秒钟的时间来处理,那么最终用户是否会以任何方式注意到这一点?不太可能。

过早优化 = 失败。

我会选择这个选项,只是因为从架构的角度来看,它最有意义。在性能成为问题之前,我并不关心它(如果确实如此,我会问自己:我需要一次连接一百万个名字吗?当然,它们不会全部出现在屏幕上......String.Format

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

评论

50赞 Aidanapword 12/17/2010
就“过早优化==失败”而言,是的。但是,当你开始为执行足迹(云和基础设施即服务,有人吗?)和/或你开始支持100万用户时,那么对单个用户请求的响应就不是问题了。为用户提供请求的成本是您的底线成本,如果/当其他数千个呼叫通过时,则会产生规模问题......
25赞 Ben Sussman 7/29/2011
这是完全错误的。在 Web 开发环境中,字符串生成代码通常位于模型、视图和控制器中,并且每次页面加载可能会被调用数万次。将评估字符串生成代码所花费的时间减少 50% 可能是一个巨大的胜利。
2赞 Phil Miller 12/30/2012
像这样的问题不仅适用于 OP 的一个实例。答案是人们在编写所有代码时可以记住的那种“我应该用哪种方式组装字符串”的事情。
6赞 BlueRaja - Danny Pflughoeft 7/3/2013
@Benjamin:......在这种情况下,你会分析并发现这是你的瓶颈。不过,我敢打赌,你只是凭空扯来的;过去编写和分析过许多 Web 应用程序,我几乎总是发现响应时间(在服务器端)的瓶颈是数据库查询。
2赞 user99999991 5/16/2015
这绝对不是过早的优化。相当谬误。字符串性能可能会完全使 UI 停滞不前,尤其是在 .NET 中,如果你正在执行大量格式设置和字符串生成。ubiquity.acm.org/article.cfm?id=1513451
5赞 2 revs, 2 users 88%David Hill #21

更好的测试是使用 Perfmon 和 CLR 内存计数器监视内存。我的理解是,您要使用 String.Format 而不仅仅是连接字符串的全部原因是,由于字符串是不可变的,因此您不必要地给垃圾回收器带来了需要在下一次传递中回收的临时字符串的负担。

StringBuilder 和 String.Format 虽然可能较慢,但内存效率更高。

字符串连接有什么不好?

评论

0赞 Marnix van Valen 5/13/2009
我同意;每个字符串操作都会创建字符串的新副本。所有这些内存迟早都会被垃圾回收器回收。所以,分配很多字符串可能会在以后回来咬你。
3赞 plinth #22

如果你正在处理一些需要易于阅读的东西(这是大多数代码),我会坚持使用运算符重载版本 UN除非:

  • 代码需要执行数百万次
  • 你正在做大量的concats(超过4个是一吨)
  • 该代码面向 Compact Framework

至少在这两种情况下,我会改用 StringBuilder。

1赞 2 revsspoulson #23

我很好奇StringBuilder在这些测试中处于什么位置。结果如下...

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
4赞 2 revs, 2 users 92%DonkeyMaster #24

我根据可读性进行选择。 当变量周围有一些文本时,我更喜欢格式选项。在此示例中:

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

即使没有变量名称,您也能理解其含义,而 concat 中充斥着引号和 + 符号,让我感到困惑:

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

(我借用了迈克的例子,因为我喜欢它)

如果没有变量名称,格式字符串就没有多大意义,我必须使用 concat:

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

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

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

在性能方面,我预计 format 选项会比 concat 慢,因为该格式需要解析字符串。我不记得必须优化这种指令,但如果我这样做了,我会看看像 和 这样的方法。stringConcat()Join()

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

3赞 Christian Hayter #25

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

1赞 Babak Naffas #26

根据 MCSD 准备材料,Microsoft 建议在处理非常少量的串联(可能是 2 到 4 个)时使用 + 运算符。我仍然不确定为什么,但这是需要考虑的事情。

22赞 Jeremy McGee #27

可怜可怜的翻译

如果你知道你的申请将保持英文,那么很好,保存时钟滴答作响。但是,许多文化通常会在例如地址中看到姓氏。

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

评论

2赞 Broots Waymb 12/10/2016
在不同的文化中会有什么不同?它不会仍然打印名字然后姓氏吗?在这两种情况下,您似乎都必须考虑不同的文化。我觉得我在这里错过了什么。string.Format()
2赞 Alex McMillan 1/12/2017
我同意@DangerZone..怎么知道你使用的是地址名称?如果根据文化进行交换,我会认为它坏了。string.Format()string.Format(){0} {1}
3赞 Richard J Foster 8/18/2017
我相信 Jeremy 试图表达的观点是,在所描述的用于支持不同国家/地区的场景中,将格式字符串本身提取到语言资源中可能是合适的。对于大多数国家/地区,该字符串将是“{0} {1}”,但对于姓氏优先是典型操作的国家/地区(例如匈牙利、香港、柬埔寨、中国、日本、韩国、马达加斯加、台湾、越南和印度部分地区),该字符串将改为“{1} {0}”。
0赞 Jeremy McGee 8/22/2017
事实上。或者,更巧妙地,将格式字符串添加为人员的属性。例如,我喜欢在我的名字后面加上我的姓氏,但我的同事 Beng 不喜欢。
37赞 3 revs, 2 users 63%MrPhil #28

字符串是不可变的,这意味着在代码中一遍又一遍地使用相同的小内存。将相同的两个字符串相加并一遍又一遍地创建相同的新字符串不会影响内存。.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

评论

1赞 mikeschuld 7/3/2013
向答案添加了 StringBuilder 和示例输出
0赞 roryok 7/27/2015
我明白使用在这里受到的微小性能打击是多么值得。从架构上讲,它更好,因为这意味着您可以更轻松地更改格式。但是字符串构建器我真的看不出有什么意义。这里的所有其他线程都说您应该使用 Stringbuilder 而不是连接字符串。有什么优势?显然不是速度,正如这个基准所证明的那样。string.Format
2赞 2 revs, 2 users 92%atlaste #29

由于我认为这里的答案没有涵盖所有内容,因此我想在这里做一个小补充。

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

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

这会将问题更改为:如何减少操作次数?concat

因此,粗略的答案是:对于 >1 个 concat,“+”对 1 个 concat 来说就好了。如果你不关心做微性能优化,在一般情况下会很好用。string.Formatstring.Format

关于文化的说明

然后有一种叫做文化的东西......

string.Format使您能够在格式中使用。简单的运算符“+”使用当前区域性。CultureInfo

如果您正在编写文件格式和 f.ex,这尤其是一个重要的评论。 “添加”到字符串的值。在不同的计算机上,如果不与显式 .doublestring.FormatCultureInfo

例如,考虑一下如果在编写逗号分隔值文件时将“.”更改为“,”会发生什么情况...在荷兰语中,小数点分隔符是一个逗号,因此您的用户可能会得到一个“有趣”的惊喜。

更详细的答案

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

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

执行此操作的最实用方法是使用过度分配策略。最常见的策略是以 2 的幂过度分配缓冲区。当然,你必须比这更聪明一点(因为如果你已经知道你需要 128 个字符,那么从 1、2、4、8 增长是没有意义的),但你明白了。该策略可确保您不需要太多我上面描述的昂贵操作。

StringBuilder是一个基本上以 2 的幂过度分配底层缓冲区的类。 在引擎盖下使用。string.FormatStringBuilder

这使得你的决策成为过度分配和追加 (-multiple)(无文化)或仅分配和追加之间的基本权衡。

8赞 5 revs, 3 users 52%Saragis #30

从 C# 开始,可以使用插值字符串来执行此操作,从而进一步简化格式。6.0

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

插值字符串表达式类似于包含表达式的模板字符串。插值字符串表达式通过将包含的表达式替换为表达式结果的 ToString 表示形式来创建字符串。

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

另请参阅这篇关于字符串插值的 dotnetperls 文章

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

5赞 von v. #31

从现在开始的一周 2015 年 8 月 19 日,这个问题将正好是七 (7) 岁。现在有一种更好的方法可以做到这一点。在可维护性方面更好,因为与仅连接字符串相比,我没有做过任何性能测试(但现在这重要吗?几毫秒的差异?使用 C# 6.0 实现此目的的新方法:

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

这个新功能更好,IMO,在我们的例子中实际上更好,因为我们有代码来构建查询字符串,其值取决于某些因素。想象一个查询字符串,我们有 6 个参数。因此,与其做一个,例如:

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

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

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

评论

0赞 Philippe 8/13/2015
事实上,C# 6.0 的新方法比以前的替代方案更好——至少从可读性的角度来看是这样。
0赞 von v. 8/13/2015
没错。而且它也更安全,因为您不必担心哪个对象进入哪个索引(占位符),因为您将直接将对象放在您想要的位置。
0赞 Philippe 10/28/2015
顺便说一句,它实际上调用了 Format(至少使用 Roslyn)。
0赞 CShark 5/27/2016
顺便说一句,这张海报所指的称为“字符串插值”,并在本线程的其他地方进行了讨论。
1赞 Misha Zaslavsky #32

最易读的是使用 C# 的字符串插值功能:6.0

Console.WriteLine($"{p.FirstName} {p.LastName}");

其性能类似于使用“+”。