String output: format or concat in C#?

2018-12-31 14:27发布

Let's say that you want to output or concat strings. Which of the following styles do you prefer?

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

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

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

Do you rather use format or do you simply concat strings? What is your favorite? Is one of these hurting your eyes?

Do you have any rational arguments to use one and not the other?

I'd go for the second one.

30条回答
查无此人
2楼-- · 2018-12-31 14:48

If you're dealing with something that needs to be easy to read (and this is most code), I'd stick with the operator overload version UNLESS:

  • The code needs to be executed millions of times
  • You're doing tons of concats (more than 4 is a ton)
  • The code is targeted towards the Compact Framework

Under at least two of these circumstances, I would use StringBuilder instead.

查看更多
不流泪的眼
3楼-- · 2018-12-31 14:50

I think this depends heavily on how complex the output is. I tend to choose whichever scenario works best at the time.

Pick the right tool based on the job :D Whichever looks cleanest!

查看更多
情到深处是孤独
4楼-- · 2018-12-31 14:50

Nice one!

Just added

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

And it is even faster (I guess string.Concat is called in both examples, but the first one requires some sort of translation).

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
查看更多
只靠听说
5楼-- · 2018-12-31 14:50

Since I don't think the answers here cover everything, I'd like to make a small addition here.

Console.WriteLine(string format, params object[] pars) calls string.Format. The '+' implies string concatenation. I don't think this always has to do with style; I tend to mix the two styles depending on the context I'm in.

Short answer

The decision you're facing has to do with string allocation. I'll try to make it simple.

Say you have

string s = a + "foo" + b;

If you execute this, it will evaluate as follows:

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

tmp here is not really a local variable, but it is a temporary for the JIT (it's pushed on the IL stack). If you push a string on the stack (such as ldstr in IL for literals), you put a reference to a string pointer on the stack.

The moment you call concat this reference becomes a problem, because there isn't any string reference available that contains both strings. This means that .NET needs to allocate a new block of memory, and then fill it with the two strings. The reason this is a problem, is because allocation is relatively expensive.

Which changes the question to: How can you reduce the number of concat operations?

So, the rough answer is: string.Format for >1 concats, '+' will work just fine for 1 concat. And if you don't care about doing micro-performance optimizations, string.Format will work just fine in the general case.

A note about Culture

And then there's something called culture...

string.Format enables you to use CultureInfo in your formatting. A simple operator '+' uses the current culture.

This is especially an important remark if you're writing file formats and f.ex. double values that you 'add' to a string. On different machines, you might end up with different strings if you don't use string.Format with an explicit CultureInfo.

F.ex. consider what happens if you change a '.' for a ',' while writing your comma-seperated-values file... in Dutch the decimal separator is a comma, so your user might just get a 'funny' surprise.

More detailled answer

If you don't know the exact size of the string beforehand, it's best to use a policy like this to overallocate the buffers you use. The slack space is first filled, after which the data is copied in.

Growing means allocating a new block of memory and copying the old data to the new buffer. The old block of memory can then be released. You get the bottom line at this point: growing is an expensive operation.

The most practical way to do this is to use an overallocation policy. The most common policy is to overallocate buffers in powers of 2. Of course, you have to do it a bit smarter than that (since it makes no sense to grow from 1,2,4,8 if you already know you need 128 chars) but you get the picture. The policy ensures you don't need too many of the expensive operations I described above.

StringBuilder is a class that basically overallocates the underlying buffer in powers of two. string.Format uses StringBuilder under the hood.

This makes your decision a basic trade-off between overallocate-and-append(-multiple) (w/w.o. culture) or just allocate-and-append.

查看更多
宁负流年不负卿
6楼-- · 2018-12-31 14:52

Try this code.

It's a slightly modified version of your code.
1. I removed Console.WriteLine as it's probably few orders of magnitude slower than what I'm trying to measure.
2. I'm starting the Stopwatch before the loop and stopping it right after, this way I'm not losing precision if the function takes for example 26.4 ticks to execute.
3. The way you divided the result by some iterations was wrong. See what happens if you have 1000 milliseconds and 100 milliseconds. In both situations, you will get 0 ms after dividing it by 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);

Those are my results:

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

查看更多
泛滥B
7楼-- · 2018-12-31 14:52

Concatenating strings is fine in a simple scenario like that - it is more complicated with anything more complicated than that, even LastName, FirstName. With the format you can see, at a glance, what the final structure of the string will be when reading the code, with concatenation it becomes almost impossible to immediately discern the final result (except with a very simple example like this one).

What that means in the long run is that when you come back to make a change to your string format, you will either have the ability to pop in and make a few adjustments to the format string, or wrinkle your brow and start moving around all kinds of property accessors mixed with text, which is more likely to introduce problems.

If you're using .NET 3.5 you can use an extension method like this one and get an easy flowing, off the cuff syntax like this:

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

Finally, as your application grows in complexity you may decide that to sanely maintain strings in your application you want to move them into a resource file to localize or simply into a static helper. This will be MUCH easier to achieve if you have consistently used formats, and your code can be quite simply refactored to use something like

string name = String.Format(ApplicationStrings.General.InformalUserNameFormat,this.FirstName,this.LastName);
查看更多
登录 后发表回答