vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】string interpolation で format string に変数を使いたかった

はじめに

この記事は C# Advent Calendar 2018 の六日目の穴埋め記事です。

以前 string interpolation を使って遊んでみたことがありました が、使っている中で困った話を。

何をしたかったか

// 「CurrentTime: 2018-12-10 21:12:23 616」 のように出力される.
Console.WriteLine($"CurrentTime: {DateTime.Now:yyyy-MM-dd HH:mm:ss fff}");

上記のようなコードで、 yyyy-MM-dd HH:mm:ss fff の部分を変数として定義し、他のコードと共有したいと思いました。

で、実際にやってみました。

string dateFormat = "yyyy-MM-dd HH:mm:ss fff";
Console.WriteLine($"CurrentTime: {DateTime.Now:dateFormat}");

出力された結果は

CurrentTime: 10a午e7or12a午

……誰だいチミは???

IL Viewer などで見たところ、 dateFormat の中身( yyyy-MM-dd HH:mm:ss fff )ではなく、「dateFormat」という文字列を format string として認識し、フォーマットをかけているようでした。

評価のタイミングが違うからおかしな文字列になるわけですね。

/(^o^)\

解決

$"CurrentTime: {DateTime.Now:{{dateFormat}}}" などあれこれ試したもののうまく解決できず。

下記のように書くのが良さそう、という結論になりました。

string dateFormat = "yyyy-MM-dd HH:mm:ss fff";
Console.WriteLine($"CurrentTime: {DateTime.Now.ToString(dateFormat)}");

う~ん、なんかしっくりこない気がするようなしないような。

とにかく問題は解決できたので良しとします。

コードを読んでみる

さて、せっかくなので、もう少し中身を見てみることにします。

ReSharper の IL Viewer を使って今回想定通り動作してくれたコードを見てみます。

まずは元のコードから。

Console.WriteLine($"CurrentTime: {DateTime.Now:yyyy-MM-dd HH:mm:ss fff}");
            
Console.WriteLine($"CurrentTime: {DateTime.Now.ToString(dateFormat)}");

では IL を見てみましょう。

// [17 13 - 17 87]
IL_0001: ldstr        "CurrentTime: {0:yyyy-MM-dd HH:mm:ss fff}"
IL_0006: call         valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now()
IL_000b: box          [System.Runtime]System.DateTime
IL_0010: call         string [System.Runtime]System.String::Format(string, object)
IL_0015: call         void [System.Console]System.Console::WriteLine(string)
IL_001a: nop          

// [21 13 - 21 84]
IL_003b: ldstr        "CurrentTime: "
IL_0040: call         valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now()
IL_0045: stloc.1      // V_1
IL_0046: ldloca.s     V_1
IL_0048: ldloc.0      // dateFormat
IL_0049: call         instance string [System.Runtime]System.DateTime::ToString(string)
IL_004e: call         string [System.Runtime]System.String::Concat(string, string)
IL_0053: call         void [System.Console]System.Console::WriteLine(string)
IL_0058: nop          

上は String.Format を、下は DateTime.ToString を使っています。

は良いのですが、上の方はボックス化されていますね。

string.Format の引数は string, object であるため、値型の DateTime を渡すとボックス化が発生する、と。

これを見ると、しっくりこないとか言っていた $"{DateTime.Now.ToString(dateFormat)}" の方が良い、という結果になりそうです。

やっぱりどのように動作するかはちゃんと確認しないとだめですね(てへぺろ)。

なお、string.Format を使って上と同じ内容を実現するコードはこちら。

Console.WriteLine($"CurrentTime: {string.Format("{0:yyyy-MM-dd HH:mm:ss fff}", DateTime.Now)}");

当然といえば当然ですが、こちらもボックス化が発生するため、 DateTime を扱う上では素直に ToString を使うのが良さそうです。

ついでに string.Format と DateTime.ToString のソースコードの URL も貼っておきます。

string.Format

string クラスは partial クラスとして処理が分割されているため、探すのにちょっと苦労しました(;゚Д゚)

しっかりとは読めていませんが、引数として渡された formatString を一文字ずつ解析し、 IFormattable を継承している DateTime クラスの ToString() を実行しているようです。

DateTime.ToString

こちらもちょろっと読んだだけですが、引数として渡された formatString を一文字ずつ解析してフォーマット、という流れは変わらないようです。

参照