LoginSignup
23
12

More than 3 years have passed since last update.

C#6 から $"{hoge}" みたいな感じで文字列に変数が埋め込めるようになったけど、フィールド変数で string a = $"{hoge}"; ってやるとエラーで怒られるのなんで??ねぇなんで??

Last updated at Posted at 2019-08-29

初学者やりがち。

public class Test{
    private string logFormat = $"{now}>{log}";

    public void Func(){
        var now = DateTime.Now;
        var log = "なんやかんやはなんやかんやですよ!";

        Console.WriteLine(logFormat); //"2019/08/29 12:52:00 > なんやかんやはなんやかんやですよ!" って出てほしい
    }
}

現在のコンテキストに 'now' という名前は存在しません。
現在のコンテキストに 'log' という名前は存在しません。

はい。エラー。なぜなら、
$"a={変数名1},b={変数名2}"string.Format("a={0},b={1}",変数名1,変数名2); にシンタックスシュガーとしてコンパイル前に変換されているから

「string.Format で、 {17} とか書いて、引数の17番目に目的の変数置くとか大変でしょ? もう何が何番目かわからないでしょ? ほらほら。僕が変換しといてあげるから~」

という親切機能でしかありません。
そのため、↑の例も内部的には

    //private string logFormat = $"{now}>{log}"; ↓に変換されている
    private string logFormat = string.Format("{0}>{1}",now,log);

と、なってるんですね。 すると、このスコープではnow,logなんてものは無いわけで。そりゃエラーにもなりますわな。

すなわち 実行時解決参照タイミングで展開されてその時のスコープの変数を展開するような仕組みではない わけです。1

それでは

public class Test{
    private string logFormat = "{now}>{log}"; //$ を外した

    public void Func(){
        var now = DateTime.Now;
        var log = "なんやかんやはなんやかんやですよ!";

        Console.WriteLine(logFormat);
    }
}

こうすればエラーは出なくなりますけどー。

{now}>{log}

当然まんま出てきちゃいますね。

じゃぁ、がんばって自分でReplaceする?

ということで、変更

public class Test{
    private string logFormat = "{now}>{log}"; //$ を外した

    public void Func(){
        var now = DateTime.Now;
        var log = "なんやかんやはなんやかんやですよ!";

        Console.WriteLine(logFormat.Replace("{now}",now.ToString()).Replace("{log}",log)); //変数の数だけReplaceが数珠つなぎ?
    }
}

2019/08/29 13:07:45>なんやかんやはなんやかんやですよ!

でるけど・・・。 出るけれどー。
もうちょっとなんとか・・・。

というわけで作ってみた

StringFormatExtension.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Util
{
    public static class StringFormatExtension
    {
        public static string Format(this string templateText, object args)
        {
            var Matches = new Regex(@"\{(.+?)\}").Matches(templateText);//{~} の間を抽出

            var index = 0;
            var objectList = new List<object>();
            foreach (Match match in Matches)
            {
                //指定されている変数名を取得する , や : などの書式指定されている場合もあるので、考慮する
                var paramName = match.Value.Substring(1, match.Value.Length - 2).Trim(); //{~}の中の中身だけ(Trim済み)
                var variableName = paramName.Split(new[] {":", ","}, StringSplitOptions.RemoveEmptyEntries)[0]; //書式文字列を抜いた、純粋な変数名
                var formatParam = paramName.Substring(variableName.Length); //変数名を抜いて書式文字列のみを取得する

                //引数 args から Reflectionを使って プロパティ or フィールド の中身を取得
                var replaceValue = args.GetType().GetProperties().Where(info => info.CanRead).FirstOrDefault(info => info.Name == variableName)?.GetValue(args)
                                   ?? args.GetType().GetFields().FirstOrDefault(info => info.Name == variableName)?.GetValue(args);

                if (replaceValue != null)
                {
                    templateText = templateText.Replace(match.Value, "{"+ index++ + formatParam + "}"); // {0}..{n}のFormat置換文字に変換
                    objectList.Add(replaceValue); //実際の値はListに入れておいて最後にstring.Formatで返却する
                }
                else
                {
                    templateText = templateText.Replace(match.Value, string.Empty);
                }
            }
            return string.Format(templateText,objectList.ToArray());
        }
    }
}

やっていることは、先ほども書いたプリコンパイラがやってくれている$付きの文字列のシンタックスシュガー

$"{hoge}"  string.Format("{0}",hoge)

を、実行時に疑似的に解決しているだけです。

使い方

拡張メソッドなので、.Formatメソッドがstringに対して生えます。(よしなにusingしてね)
引数に渡すのは object です。 今回のようにローカル変数の場合は匿名クラスにしてしまうと、プロパティ名がローカル変数名に設定されるので、らくちんです。

public class Test{
    private string logFormat = "{now}>{log}";

    public void Func(){
        var now = DateTime.Now;
        var log = "なんやかんやはなんやかんやですよ!";

        Console.WriteLine(logFormat.Format(new{now,log}));
    }
}

匿名クラスじゃなくて、既存インスタンスを渡してもいいですが、Field/Property に外からアクセスすることになるので、publicである必要があるので要注意です。

public class Test{
    private string logFormat = "{now}>{log}";
    public DateTime now => DateTime.Now; //外からアクセスされるのでpublicじゃないとだめ!
    public string log;                   //外からアクセスされるのでpublicじゃないとだめ!

    public void Func(){
        log = "なんやかんやはなんやかんやですよ!";

        Console.WriteLine(logFormat.Format(this)); //thisを渡す!
    }
}

便利! かな・・・?

追記(2019/08/30)

コメントなどで「なんでそんな面倒臭いことを!?、普通に指定すればいいのでは!?(要約)」等々

  • いったい、こいつは何の役に立つの?

という反応がありました。 確かにこの記事だけ見ると「で?」となるのもイタシカタガナイ。

言葉足らずで申し訳ない。
この拡張メソッドの利用ケースは「外部設定データ(など)で画面などに表示する文言を変更したい」を想定していました。

たとえば、エラーログのある1行

2019/09/01 10:10:00 > エラーが発生しました (errorCode:4101 ,pId:1000)

これには

  • TimeStamp
  • エラーメッセージ
  • エラーコード
  • プロセスID

が出ています。

それぞれ

  • TimeStamp timeStamp
  • エラーメッセージ errorMessage
  • エラーコード errorCode
  • プロセスID pId

と変数で定義されているとして、

LogOutput($"{timeStamp} > {errorMessage} (errorCode:{errorCode} ,pId:{pId})");

こうすれば、目的のLogがでます。よしよし。

そんな中、しばらくそのアプリが動いた後、ふとログを見た偉い人がこう言います。
「pIDってなに? あ、プロセスID? じゃぁ、プロセスIDって書いておいてよ :rage:
あっはいー。しょうがないなぁ。なんでそんなどうでもいいことを・・・ブツブツ・・・。 修正してビルドビルドっと。

LogOutput($"{timeStamp} > {errorMessage} (errorCode:{errorCode} ,プロセスID:{pId})");

そしてまたある日
「Excelにコピペしたいからさ、各項目Tab(orカンマ)で区切ってくれない?:smirk:

ぐぬぬ・・・。

みなさん、こんなやり取りをあと10回ほど繰り返しますよね?(ブラック

でも、ちょっとまって。辞表を投げつけるのはちょっと早い。


こんな時のために、 フォーマットはソースコードに書かずに、設定ファイルなどに置いておきたい そして
「あ、それ設定ファイルで変更できるので、お好きにどうぞ。」
と言いたい。(ことがしばしばあります。(よね?)(まぁ、大体言えない))

なので、設定ファイル(別にDBのマスタデータでもよいです)にこんな感じに書きたい。

設定ファイル
<logFormat>{timeStamp} > {errorMessage} (errorCode:{errorCode} ,プロセスID:{pId})</logFormat>
var template = config.logFormat;//まぁ、こんな感じで↑の</logFormat>~</logFormat>が取れるとしよう。
LogOutput(template); // TODO あれ、ここどうしよう。

こうやって書いても変数展開されないのは、前述したとおり。 あ、Replace4連発だ。これ。 と気付きます。

ここで、ここで、ようやく作った拡張メソッドが役に立ちます。

var template = config.logFormat;//まぁ、こんな感じで↑の</logFormat>~</logFormat>が取れるとしよう。
LogOutput(template.Format(new{errorMessage,timeStamp,errorCode,pId});

やったね!


そもそも、よく見る解決方法としては

  • TimeStamp {0}
  • エラーメッセージ {1}
  • エラーコード {2}
  • プロセスID {3}

として、 {n} の箇所は置換されるよ!」 と決め打ちするパターン
設定ファイルはこんな感じに

設定ファイル
<logFormat>{0} > {1} (errorCode:{2} ,プロセスID:{3})</logFormat>

プログラムはこんな感じに

var template = config.logFormat;//まぁ、こんな感じで↑の</logFormat>~</logFormat>が取れるとしよう。
LogOutput(string.Format(template,timeStamp,errorMessage,errorCode,pId));

(書いてて、「まぁ、これでもいいよね」という気にもなってきてしまっているが、負けない!)

この、{0}とか{1}とか、超マジックナンバーが設定ファイルに横行するのを嫌がっているわけです。 どうでしょうか、そう考えるとこの拡張メソッドの価値も見えてきたりしませんでしょうか?

え。 「プログラム中にどんな変数名で宣言しているか知らないと指定できないじゃないか」って? そうですね。でも、{0}や{1}みたいにプログラム中のstring.Formatへの引数の順番を知らないと指定できないのと同じですし、だったらただの数字よりは人間にやさしくないですか?

あ、はい。 「リファクタリングにめちゃ弱いじゃないか?」と。 ん・・はい・・・そうですね。変数名で探しに行き・・・ますもんね・・・。たしかに・。

っ、は・はひっ・・。 「Reflection使ってまですることか。」と。 えぇ、はい。そうですね。・・・すみませんでした・・。

そんなにいじめないで。辞表投げつけたくなっちゃう。

追記終了

最後に

  • こっそり、書式指定も対応しています。なので "{now:yyyyMMdd}" なんて風にDateTimeの書式指定なんかもできます。
  • 式木使えばもっとなんと便利になるんじゃないかなーとも思いましたが、とりあえずやめておきました。

参考

http://neue.cc/2013/01/05_392.html
neuec神に感謝!


  1. 「実行時解決はしている」とコメントを頂きました、確かに間違った用語を使ってしまっていたので修正させていただきました。ご指摘ありがとうございます。 

23
12
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12