C#の文字列補間という機能をしっていますか?
以下のように文字列の中に変数を埋め込める機能です。
var s = "world";
Console.WriteLine($"hello {s}");
// hello world
C#に限らずほとんどの言語で使用できる機能です。
C#独自の要素として文字列補間を使用した場合、FormattableString
として変数に代入できるというものがあります。
// 左辺をFormattableStringにできる
FormattableString s = $"";
// 文字列補間を使用しない場合は代入できない
// 型 'string' を 'System.FormattableString' に暗黙的に変換できません
FormattableString s = "";
// varを使用した場合はstringになる
var s = $"";
参考:文字列補間を使用してカルチャ固有の結果文字列を作成する方法
この仕様自体はカルチャ関連のために存在しているみたいですがほとんど使用したことがないのでよくわかりません。
今回はこの機能を使用して遊んでみます。
実用性は多分ないです。
FormattableStringの仕様
FormattableString
は文字列補間に使用されるフォーマット、渡された引数を持ったオブジェクトです。
var s = "world";
FormattableString fs = $"hello {s}!";
Console.WriteLine(fs.Format);
Console.WriteLine(fs.ArgumentCount);
Console.WriteLine(fs.GetArguments()[0]);
// 出力
// hello {0}!
// 1
// world
ポイントはフォーマット文字列と渡された引数が分けて取得できることです。
つまり以下ようなメソッドを書くことができます。
// 挿入される文字列を2倍にするメソッド
string DoubleInsert(FormattableString fs)
{
var f = fs.Format;
for (var i = 0; i < fs.ArgumentCount; i++)
{
var target = "{" + i + "}";
f = f.Replace(target, target + target);
}
return string.Format(f, fs.GetArguments());
}
var s = "world";
Console.WriteLine($"hello {s}!");
Console.WriteLine(DoubleInsert($"hello {s}!"));
// 出力
// hello world!
// hello worldworld!
何に使えるかわからない非常に面白い機能だと思います。
何とかして有効活用できる方法を探します。
テンプレートとして使用する
長い文字列を生成する際に以下のようなメソッドを作るかもしれません。
static string Template(int i, int j)
{
return $@"
i: {i}
j: {j}
";
}
この文字列をそのまま使用するなら大した問題はありませんが例えばこの文字列をパースする場合を考えます。
つまりこういうことです。
string Template(int i, int j)
{
return $@"
i: {i}
j: {j}
";
}
Dictionary<string, string> CreateDictionary(int i, int j)
{
var dict = new Dictionary<string, string>();
// 文字列を作る
var s = Template(i, j);
// 文字列をパースしてDictionary<string,string>に変換する
foreach (var line in s.Split('\n'))
{
var x = line.Split(":");
if (x.Length != 2) continue;
dict[x[0].Trim()] = x[1].Trim();
}
return dict;
}
var d = CreateDictionary(100, 200);
foreach (var kv in d)
{
Console.WriteLine($"key:{kv.Key} value:{kv.Value}");
}
// 出力
// key:i value:100
// key:j value:200
上記のCreateDictionary
はあんまり効率が良くないので書き直すことを考えます。
理想は以下の形ですがこれではあまり柔軟性がありません。
Dictionary<string, string> CreateDictionary(int i, int j)
{
return new Dictioanry<string, string>()
{
["i"] = i.ToString(),
["j"] = j.ToString(),
};
}
ある程度汎用的にするなら目指すのは以下のような形でしょうか。
var template = @"
i: _
j: _
";
// テンプレートからキーの部分のみを取り出す
var keys = GetKeys(template);
Dictionary<string, string> CreateDictionary(object[] values)
{
var dict = new Dictionary<string, string>();
for (var i = 0; i < keys.Length; i++)
{
dict[keys[i]] = values[i].ToString();
}
return dict;
}
これを文字列補間を使用して何とかできないか考えてみましたが最終的に以下のような書き方ができるようになりました。
実装は長いので省きます。
興味がある方はこちらを参照してください。
// DictionaryBuilder.Createでテンプレートを作成
// ネストも可能
var builder = DictionaryBuilder.Create<(int i, int j)>(p => $@"
i: {p.P(t => t.i)}
j: {p.P(t => t.j)}
jj: {p.P(t => t.j + t.j)}
dict: {p.P(t => t.i, DictionaryBuilder.Create<int>(pp => $@"
s: 1
v: {pp.P(v => v)}
"))}
");
// テンプレートを元にDictionaryを作成
foreach (var kv in builder.ToDictionary((30, 40)))
{
if (kv.Value is Dictionary<string, object> dic)
{
Console.WriteLine($"key:{kv.Key}");
foreach (var kv2 in dic)
{
Console.WriteLine($" key:{kv2.Key} value:{kv2.Value}");
}
}
else
{
Console.WriteLine($"key:{kv.Key} value:{kv.Value}");
}
}
/* output
key:i value:30
key:j value:40
key:jj value:80
key:dict
key:s value:1
key:v value:30
*/
なんかそれっぽい形になったと思います。
ゼロアロケーション文字列補間
1バイトもアロケーションしたくない人にとっては素の文字列補間は使用できません。
ゼロアロケーションを目指す場合はフォーマット文字列と引数を別々に渡し、フォーマット文字列をパースしつつ事前に確保しておいたバッファに文字をためていくという実装になります。
例えばUnityのTextMeshProは以下の形でゼロアロケーションを実現しています。
TextMeshPro text = /* ... */;
// SetTextを行うとTextMeshProが内部に持っているバッファに文字情報が設定される
// 内部に持っているバッファはcharの配列なので文字列をアロケーションする必要がない
// 逆に言うと文字列にできないのでフォーマットのパースやchar配列への変換などは全部自力でやらなければならない
text.SetText("i: {0} j: {1}", 1, 2);
文字列補間を使用したゼロアロケーションは以下のような書き方ができるようになりました。
実装はこちらです。
// 文字情報を内部のchar配列にためるBuilder
var sb = new CharBufferedStringBuilder();
// 文字列補間からテンプレートの作成
var fsb = new FormatStringBuilder<(int i, int j)>(p => $"i: {p.P(t => t.i)}, j: {p.P(t => t.j)}");
// テンプレートを適用
// バッファサイズが足りなくならない限りはゼロアロケーション
// 適用時に毎回パースしなくて済むので多少効率的(かもしれない)
fsb.Apply(sb, (100, 200));
Console.WriteLine(new string(sb.Buffer, 0, sb.Length));
/* output
i: 100, j: 200
*/
最後に
今回の記事はjavascriptのタグ付けされたTemplate literalがうらやましかったので作成しました。
タグ付けされたTemplate literalの使用用途について探してもstyled-componentsかgraphql-tagくらいしか見つけられなかったのでC#ではあまり使えない予感がします。
実用的に使えそうなものがあればぜひ教えてください。
実装したコードは以下に置いています。
yaegaki/StringInterpolationUtil