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

Phi*_*ppe 175 c# string string.format coding-style

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

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

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

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

你更喜欢使用格式还是简单地连接字符串?什么是你最喜欢的?其中一个伤害了你的眼睛吗?

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

我会去第二个.

Fre*_*eth 156

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

过早优化=失败.

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

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

  • 关于"过早优化==失败"的好处,是的.但是,当你开始支付执行足迹(云和基础设施作为服务,任何人?)和/或你开始支持100万用户的某些东西时,对请求的单个用户的响应不是问题.为用户提供服务的成本是您的底线成本以及如果/当​​其他几千个电话通过时的规模问题...... (47认同)
  • 这完全错了.在Web开发环境中,您的字符串生成代码通常会在模型,视图和控制器中深入,并且每页加载可能会被调用数万次.缩短将字符串生成代码评估50%所花费的时间可能是一个巨大的胜利. (23认同)
  • @Benjamin:......在这种情况下,你会发现并发现这是你的瓶颈.不过,我敢打赌,你只是把它从无处拉出来; 在过去编写和分析了许多webapps时,我几乎总是发现响应时间的瓶颈*(在服务器端)*是数据库查询. (4认同)
  • 像这样的问题不只是在OP的一个实例中应用.答案是人们可以记住的那种"我应该用哪种方式组装字符串?" 因为他们写了*他们的代码. (2认同)
  • 这绝对不是过早的优化.相当谬论.字符串性能可以完全阻止UI,特别是在.NET中,如果你正在进行大量的格式化和字符串构建.http://ubiquity.acm.org/article.cfm?id=1513451 (2认同)
  • 如果我不必处理与使用string.format()直接相关的性能问题,我会同意你的意见.处理使用大量string.format()调用的数据繁重的业务应用程序,因为它很容易,但是当处理1000多个并发用户时,页面上的20个string.format调用快速加起来 (2认同)

Mic*_*ski 88

试试这个代码.

它是您的代码的略微修改版本.
1.我删除了Console.WriteLine,因为它可能比我想要测量的速度慢几个数量级.
2.我在循环之前启动秒表并在之后立即停止它,这样我就不会失去精度,如果该函数需要例如26.4滴答来执行.
你通过一些迭代划分结果的方式是错误的.看看如果你有1000毫秒和100毫秒会发生什么.在这两种情况下,将它除以1000000后将得到0 ms.

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);
Run Code Online (Sandbox Code Playgroud)

这是我的结果:

1000000 x result = string.Format("{0} {1}",p.FirstName,p.LastName); 采取:618ms - 2213706 ticks
1000000 x result =(p.FirstName +""+ p.LastName); 花了:166ms - 595610滴答

  • 字符串是不可变的,这意味着在代码中反复使用相同的小块内存.将相同的两个字符串添加到一起并一遍又一遍地创建相同的新字符串不会影响内存..Net非常聪明,只是为了使用相同的内存引用.因此,您的代码并没有真正测试两个concat方法之间的区别.请参阅下面的答案中的代码. (31认同)

sam*_*son 54

哦亲爱的 - 在阅读其中一个回复后,我试图颠倒操作的顺序 - 所以首先执行连接,然后是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
Run Code Online (Sandbox Code Playgroud)

因此,操作的顺序会产生巨大差异,或者说第一次操作总是慢得多.

以下是不止一次完成操作的运行结果.我曾尝试更改订单,但一旦第一个结果被忽略,事情通常遵循相同的规则:

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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,相同方法的后续运行(我将代码重构为3种方法)逐渐加快.最快的似乎是Console.WriteLine(String.Concat(...))方法,然后是正常连接,然后是格式化的操作.

启动时的初始延迟可能是Console Stream的初始化,因为在第一次操作之前放置Console.Writeline("Start!")会使所有时间重新开始.

  • 然后从测试中完全删除Console.WriteLine.它扭曲了结果! (2认同)

Lud*_*ton 36

字符串是不可变的,这意味着在代码中反复使用相同的小块内存.将相同的两个字符串添加到一起并一遍又一遍地创建相同的新字符串不会影响内存..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();
Run Code Online (Sandbox Code Playgroud)

样本输出:

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
Run Code Online (Sandbox Code Playgroud)


Jer*_*Gee 22

可怜的翻译

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

所以请使用string.Format(),特别是如果您将应用程序转到英语不是第一语言的任何地方.

  • `string.Format()`在不同的文化中表现如何?难道它还不会打印名字和姓氏吗?在这两种情况下,您似乎都必须考虑不同的文化.我觉得我在这里错过了一些东西. (2认同)
  • 我同意@DangerZone ..如何`string.Format()`知道你在使用地址的名字?如果`string.Format()`基于文化交换`{0} {1}`,我会认为它被打破了. (2认同)
  • 我相信 Jeremy 试图说明的一点是,在所描述的支持不同国家/地区的场景中,将格式字符串本身提取到语言资源中可能是合适的。对于大多数国家/地区,该字符串将是“{0} {1}”,但对于姓氏优先是典型操作的国家/地区(例如匈牙利、香港、柬埔寨、中国、日本、韩国、马达加斯加、台湾、越南和印度部分地区)该字符串将改为“{1} {0}”。 (2认同)

Phi*_*ppe 14

以下是超过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
Run Code Online (Sandbox Code Playgroud)

以下是替补代码:

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");
Run Code Online (Sandbox Code Playgroud)

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


Nat*_*han 9

连接字符串在这样的简单场景中很好 - 它比任何更复杂的东西更复杂,甚至是LastName,FirstName.使用这种格式,您可以一目了然地看到,在读取代码时字符串的最终结构是什么,通过连接,几乎不可能立即识别最终结果(除非有一个非常简单的例子).

从长远来看,这意味着当你回来改变你的字符串格式时,你要么能够弹出并对格式字符串进行一些调整,要么皱起眉头并开始四处移动各种属性访问器与文本混合,更容易引入问题.

如果您使用的是.NET 3.5,那么您可以使用像这样的扩展方法,并获得一个简单的流程,关闭袖口语法,如下所示:

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

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

string name = String.Format(ApplicationStrings.General.InformalUserNameFormat,this.FirstName,this.LastName);
Run Code Online (Sandbox Code Playgroud)


小智 7

对于非常简单的操作,我会使用连接,但是一旦超出2或3个元素格式变得更合适IMO.

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


Ada*_*ile 6

虽然我完全理解了我的第一个答案的风格偏好和选择连接,部分是基于我自己的偏好,但我的部分决定是基于连接速度更快的想法.因此,出于好奇,我对它进行了测试,结果非常惊人,特别是对于如此小的字符串.

使用以下代码:

    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");
Run Code Online (Sandbox Code Playgroud)

我得到了以下结果:

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
Run Code Online (Sandbox Code Playgroud)

使用格式化方法慢了100多倍!连接甚至没有注册为1ms,这也是我输出计时器滴答的原因.

  • 但是,您当然应该多次执行操作以进行测量. (2认同)
  • 并且失去对Console.Writeline()的调用,因为它超出了问题的范围? (2认同)

sam*_*son 5

通常我更喜欢前者,特别是当字符串变长时,它可以更容易阅读.

另一个好处是我相信性能之一,因为后者在将最终字符串传递给Console.Write方法之前实际执行了2个字符串创建语句.String.Format在我认为的封面下使用StringBuilder,因此避免了多个连接.

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


Mik*_*ike 5

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

只保留了一些字符,但我认为,在这个例子中,格式使它更清晰.


Dav*_*ill 5

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

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

字符串连接有什么坏处?


Sar*_*gis 5

从C#6.0开始,内插字符串可用于执行此操作,从而进一步简化了格式.

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

插值字符串表达式看起来像包含表达式的模板字符串.插值字符串表达式通过将包含的表达式替换为表达式结果的ToString表示来创建字符串.

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

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

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


von*_* v. 5

从现在到2015年8月19日一周,这个问题将正好七(7)年.现在有一种更好的方法.在可维护性方面更好,因为我没有进行任何性能测试而只是连接字符串(但这些日子是否重要?差异几毫秒?).使用C#6.0实现它的新方法:

var p = new { FirstName = "Bill", LastName = "Gates" };
var fullname = $"{p.FirstName} {p.LastName}";
Run Code Online (Sandbox Code Playgroud)

这个新功能更好,IMO,在我们的情况下实际上更好,因为我们有代码,我们构建的查询字符串的值取决于某些因素.想象一下我们有6个参数的查询字符串.因此,而不是做一个,例如:

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

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

var qs=$"q1={someVar}&q2={anotherVarWithLongName}&q3={var3}&q4={var4}&q5={var5}&q6={var6}";
Run Code Online (Sandbox Code Playgroud)