Edited at

C# のstring.Format()と文字列補間


C# のstring.Format()と文字列補間

C#にはフォーマット文字列を扱う手段として、以下の2つが存在する。


  • string.Format(string,object)

  • $"{}"

$"{}"の方が新しい書き方で、C#6.0から使用できる。 

この記法は文字列補間(string interpolation)と呼ばれる。

メリットは以下の通り。


  • 変数を直感的に代入できる

  • フォーマット指定子も使用可能

  • 早くなる(らしい)


変数の代入

// string.Format()

var text2 = string.Format("Value is {0}",v);
var text1 = string.Format("Value is {0},{1},{2}",v ,v2 ,v3);

// string interpolation

var text0 = $"Value is {v}";
var text1 = $"Value is {v},{v1},{v2}");

このようになる。

注意深く見ると、{0}は文字列{v}は式として解釈されているのが分かる。

つまりstring interpolationは変数の補完が可能になる。 

これによって変数のindex指定ミスによる順序のズレはかなり起きづらくなる。


フォーマット指定子

string.Format()とほとんど変わらない記法でフォーマット指定子を利用することが出来る。

string text = $"My value is {10.22445:F3}";

Console.WriteLine(text);

// >> My value is 10.224

従来通り、フォーマット指定子の補間は効かない点に注意すること。


早くなる

そんなまさか...

気になったのでやってみる。

環境は以下の通り


  • Windows 10

  • VisualStudio2019

  • .Net Core 2.2.5

ベンチマークにはBenchmarkDotNetを使用する


StringFormat.cs

[RPlotExporter, RankColumn]

public class StringFormat
{
[Params(0)]
public int val;
public List<int> items = new List<int>();

[Benchmark(Description = "string.Format(string,object)")]
public void T1()
{
string string1 = string.Format("My value is {0}", val);
}
[Benchmark(Description = "string.Format(string,string)")]
public void T3()
{
var text = val.ToString();
string string1 = string.Format("My value is {0}", text);
}
[Benchmark(Description = "$(object)")]
public void T2()
{
string string1 = $"My value is {val}";
}
[Benchmark(Description = "${string}")]
public void T4()
{
var text = val.ToString();
string string1 = $"My value is {text}";
}
}



result.log

|                       Method | val |      Mean |     Error |    StdDev | Rank |

|----------------------------- |---- |----------:|----------:|----------:|-----:|
| string.Format(string,object) | 0 | 137.45 ns | 2.6710 ns | 2.4985 ns | 2 |
| string.Format(string,string) | 0 | 148.95 ns | 2.9784 ns | 5.5206 ns | 3 |
| $(object) | 0 | 139.29 ns | 2.7419 ns | 4.2689 ns | 2 |
| ${string} | 0 | 36.81 ns | 0.7249 ns | 0.6426 ns | 1 |

思ってた結果と大分違う。

string.Format(string,object)

string.Format(string,string)

で差があるのは、string.Format(string,string)というオーバーロードが存在しないため

val.ToString()が無駄になっているからだと思われる。

際立って早い$"{string}"のILを確認してみる。

確認にはildasm.exeを使用した


$"{string}".IL

  .maxstack  2

.locals init (string V_0)
IL_0000: ldarg.0
IL_0001: ldflda int32 ConsoleApp2.StringFormat::val
IL_0006: call instance string [System.Runtime]System.Int32::ToString()
IL_000b: stloc.0
IL_000c: ldstr "My value is "
IL_0011: ldloc.0
IL_0012: call string [System.Runtime]System.String::Concat(string, string)
IL_0017: pop
IL_0018: ret


$"{object}".IL

  .maxstack  2

.locals init (string V_0)
IL_0000: ldarg.0
IL_0001: ldflda int32 ConsoleApp2.StringFormat::val
IL_0006: call instance string [System.Runtime]System.Int32::ToString()
IL_000b: stloc.0
IL_000c: ldstr "My value is {0}"
IL_0011: ldloc.0
IL_0012: call string [System.Runtime]System.String::Format(string, object)
IL_0017: pop
IL_0018: ret

見ての通り、$"{string}"ではstring.Concat()が呼び出されている。

string.Format(string,string)は最適化がうまく掛からず、

string.Concat()ではなくstring.Format()になってしまうようだ。

object型が混じるとうまく最適化されない(string.Formatに置き換えられる)ようなので、

string interpolationに入れる前に、

予め使用する変数を.ToString()しておくと良さそうだ。


まとめ

$"{string}"を使おう。