For anyone who is curious, here is the IL for the OP's example.
IL_0001: ldstr "persian"
IL_0006: stloc.0
IL_0007: ldstr "I have a wild {0} cat that likes to visit"
IL_000c: ldloc.0
IL_000d: call string [mscorlib]System.String::Format(string, object)
IL_0012: call void [mscorlib]System.Console::WriteLine(string)
IL_0017: nop
IL_0018: ldstr "I have a wild "
IL_001d: ldloc.0
IL_001e: ldstr " cat that likes to visit"
IL_0023: call string [mscorlib]System.String::Concat(string, string, string)
IL_0028: call void [mscorlib]System.Console::WriteLine(string)
Personally I opt for String.Format() because it does so much for so little.
Consider this example:
Console.WriteLine("I have " + count.ToString() + " wild cats that like to visit");
Which results with this IL:
IL_0030: ldstr "I have "
IL_0035: ldloca.s count
IL_0037: call instance string [mscorlib]System.Int32::ToString()
IL_003c: ldstr " wild cats that like to visit"
IL_0041: call string [mscorlib]System.String::Concat(string, string, string)
IL_0046: call void [mscorlib]System.Console::WriteLine(string)
In comparison, using String.Format
Console.WriteLine(String.Format("I have {0} wild cats that like to visit", count));
...is slightly better.
IL_0096: ldstr "I have {0} wild cats that like to visit"
IL_009b: ldloc.1
IL_009c: box [mscorlib]System.Int32
IL_00a1: call string [mscorlib]System.String::Format(string, object)
IL_00a6: call void [mscorlib]System.Console::WriteLine(string)
Not convinced? How about this realistic example with two substitution parameters including a date (I'm often inserting a date into log strings, for example).
The bad way:
Console.WriteLine("Today is " + System.DateTime.Now.ToString("M/d/yy") + " and I have " + count.ToString() + " wild cats that like to visit");
Insane block of IL this time:
IL_004c: ldc.i4.5
IL_004d: newarr [mscorlib]System.String
IL_0052: stloc.2
IL_0053: ldloc.2
IL_0054: ldc.i4.0
IL_0055: ldstr "Today is "
IL_005a: stelem.ref
IL_005b: ldloc.2
IL_005c: ldc.i4.1
IL_005d: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
IL_0062: stloc.3
IL_0063: ldloca.s CS$0$0001
IL_0065: ldstr "M/d/yy"
IL_006a: call instance string [mscorlib]System.DateTime::ToString(string)
IL_006f: stelem.ref
IL_0070: ldloc.2
IL_0071: ldc.i4.2
IL_0072: ldstr " and I have "
IL_0077: stelem.ref
IL_0078: ldloc.2
IL_0079: ldc.i4.3
IL_007a: ldloca.s count
IL_007c: call instance string [mscorlib]System.Int32::ToString()
IL_0081: stelem.ref
IL_0082: ldloc.2
IL_0083: ldc.i4.4
IL_0084: ldstr " wild cats that like to visit"
IL_0089: stelem.ref
IL_008a: ldloc.2
IL_008b: call string [mscorlib]System.String::Concat(string[])
IL_0090: call void [mscorlib]System.Console::WriteLine(string)
Using String.Format:
Console.WriteLine(String.Format("Today is {0:M/d/yy} and I have {1} wild cats that like to visit", DateTime.Now, count));
Much much shorter than the other version:
IL_00ac: ldstr "Today is {0:M/d/yy} and I have {1} wild cats that like to visit"
IL_00b1: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
IL_00b6: box [mscorlib]System.DateTime
IL_00bb: ldloc.1
IL_00bc: box [mscorlib]System.Int32
IL_00c1: call string [mscorlib]System.String::Format(string, object, object)
IL_00c6: call void [mscorlib]System.Console::WriteLine(string)
The only thing that bothers me a little bit is the boxing operation, since String.Format only takes objects for the substitution parameters. From every other perspective, it is easier to read and results in more efficient IL.
string.concat
orlist.join
... etc. etc. I think the best option is to understand the underlying mechanics of what you are writing and choose the right tool. Check out: joelonsoftware.com/articles/fog0000000319.html \$\endgroup\$