【C#】string-interpolation(文字列補間)の使い方とメリット
序論
なんか最近仕事してて、自分が当たり前と思っていることでも、そうじゃない人もいるんだなぁと改めて思った。これは考えてみれば当然のことではあるんだけど。
先日C#のコードを書いていたら、それを見た人から「これ何?」と聞かれた。C#のstring-interpolation
(文字列補間)の書式が分からないらしかった。私はstring.Format
よりも文字列補間式を好むのだが、まぁ分からなければ難しいのだろうか?学習コストはそこまで高くないと個人的には思うし、こういうことがあるのでせっかくならまとめてみようと思った。
まぁそんな固く考えなくてもとりあえず書いちゃえって感じなので書きます。ついでにstring.Format
についてもおさらい。
string.Format
string型にはFormat
というメソッドがある。これは文字列の中に{0}
などのプレースホルダを設定しておくと、引数をそこに入れてくれるというものだ。
const string name = "あぱしょに";
const int age = 27;
string text = "私の名前は{0}です。年齢は{1}歳です。";
string formattedText = string.Format(text, name, age);
// 結果 -> 私の名前はあぱしょにです。年齢は27歳です。
ちなみにパラメータ引数はobject型を取って内部でToString
しているので型は意識しなくても良い。params object[]を取るので型がバラバラでも構わない。
プレースホルダは同じインデックスを指定するとまとめて入れることもできる。
const string favoriteName = "リンゴ";
const string favoritePoint = "甘い";
string text = "私の好物は{0}です。{0}の好きなところは、{1}ところです。";
string formattedText = string.Format(text, favoriteName, favoritePoint);
// 結果 -> 私の好物はリンゴです。リンゴの好きなところは、甘いところです。
第2パラメータ以降で必要な数は、プレースホルダの数ではなくプレースホルダの種類(割り当てたインデックス)の数である。プレースホルダが2種類で合計3個なら、第2パラメータ以降は2つ指定する。
書式設定を指定することも出来る。プレースホルダのインデックスの後に:
を挟んで書式設定を指定する。
DateTime Nov042020 = new DateTime(2020, 11, 4);
string text = "今日は{0:yyyy年MM月dd日}です。";
string formattedText = string.Format(text, Nov042020);
// 結果 -> 今日は2020年11月04日です。
拡張メソッド化
string.Formatをいちいち書くのは面倒なので、拡張メソッドを用意している人も多いと思う。
public static class StringExtension
{
public static string FormatEx(this string text, params object[] parameters) => string.Format(text, parameters);
}
たとえば上記のようなメソッドを用意しておけば、
DateTime Nov042020 = new DateTime(2020, 11, 4);
string text = "今日は{0:yyyy年MM月dd日}です。";
string formattedText = text.FormatEx(Nov042020);
// 結果 -> 今日は2020年11月04日です。
のように使える。こういう拡張メソッドを用意しておくとタイプ量も減るしテストもしやすいし可読性も上がると個人的には思う。
string.Formatのイマイチな点
2点ある。
- パラメータがどのプレースホルダに入るのか分かりづらい
- コンパイルエラーを検出してくれない
まずは1点目。プレースホルダが少なければ別に問題ないのだが、たとえば
const string LangName = "C#";
const string LangName_ja = "シーシャープ";
const string DevelopersFirstName = "アンダース";
const string DevelopersLastName = "ヘルスバーグ";
const string ProgrammingLang = "プログラミング言語";
const string LangsGroupOfC = "C系言語";
const string SampleLang1 = "C言語";
const string SampleLang2 = "C++";
const string SampleLang3 = "Java";
const string DevelopersOffice = "ボーランド";
const string SampleLang4 = "Delphi";
string text = "{0}({1})は、{2}・{3}が設計した{4}であり、構文はその名前にもある通り{5}({6}、{7}や{8}など)の影響があるが、構文以外の言語機能などについては{3}が以前の所属である{9}で設計した{10}からの影響がある。";
string formattedText = string.Format(text, LangName, LangName_ja, DevelopersFirstName, DevelopersLastName, ProgrammingLang, LangsGroupOfC, SampleLang1, SampleLang2, SampleLang3, DevelopersOffice, SampleLang4);
// 結果 -> C#(シーシャープ)は、アンダース・ヘルスバーグが設計したプログラミング言語であり、構文はその名前にもある通りC系言語(C言語、C++やJavaなど)の影響があるが、構文以外の言語機能などについてはヘルスバーグが以前の所属であるボーランドで設計したDelphiからの影響がある。
上記のような長ったらしい文字列の補間などは目で追う気にもならない。はっきりと言って苦痛である。(例文はC#のwikipediaのページより拝借した)
こういうコードを書く方が問題だといえば当然そうなのだが、しかし現実問題として先人が書いたこういうコードを読むことが何度もあった。
こういうふうにプレースホルダとパラメータが多いと、対応するパラメータを探すのだけで一苦労であるし、上記では{3}
が2回登場しているが、その度にパラメータをまた辿りなおすのも面倒である。
整形後のイメージがコメントで書いてあればまだマシだが、そこまで気が利くような人はこんな長ったらしいFormat
を書かないのである。
そして2点目、string.Format
はコンパイルエラーを検知できない。
const string favoriteName = "リンゴ";
const string favoritePoint = "甘い";
string text = "{0}の好きなところは、{1}ところです。";
string formattedText = string.Format(text, favoriteName); // パラメータに favoritePoint を指定するのを忘れている
// 結果 -> 実行時エラー
上記のコードはプレースホルダに対してパラメータが不足しているため、下記の実行時エラーが発生する。
System.FormatException: ‘インデックス (0 ベース) は 0 以上で、引数リストのサイズよりも小さくなければなりません。’
しかし、string.Format
の構文としては正しい(第1引数がstring、第二引数がobject)ため、コンパイルエラーとして検知されない。つまり、実行してみるまでエラーになるか分らないのだ。
上記のような簡単なコードであれば原因も分かるが、「1.パラメータがどのプレースホルダに入るのか分かりづらい」の説明に例示したような複雑に入り組んだコードの場合は原因を探すモチベーションすら湧いてこないだろう。
上記の問題は、いずれもstring-interpolation
(文字列補間)を利用することで解消できる。
string-interpolation(文字列補間)
この機能はC# 6.0から利用できるようになっている。
利用するには、文字列の最初に$
を付けて、プレースホルダは{}
で囲んだ中に直接値(あるいは変数・定数など)を書き込む。
const string name = "あぱしょに";
const int age = 27;
string formattedText = $"私の名前は{name}です。年齢は{age}歳です。";
// 結果 -> 私の名前はあぱしょにです。年齢は27歳です。
書式設定を指定する時は、値の後に:
を挟み指定することができる。
DateTime Nov042020 = new DateTime(2020, 11, 4);
string formattedText = $"今日は{Nov042020:yyyy年MM月dd日}です。";
// 結果 -> 今日は2020年11月04日です。
という具合だ。
また、プレースホルダが多い場合でも
const string LangName = "C#";
const string LangName_ja = "シーシャープ";
const string DevelopersFirstName = "アンダース";
const string DevelopersLastName = "ヘルスバーグ";
const string ProgrammingLang = "プログラミング言語";
const string LangsGroupOfC = "C系言語";
const string SampleLang1 = "C言語";
const string SampleLang2 = "C++";
const string SampleLang3 = "Java";
const string DevelopersOffice = "ボーランド";
const string SampleLang4 = "Delphi";
string formattedText = $"{LangName}({LangName_ja})は、{DevelopersFirstName}・{DevelopersLastNameが設計した{ProgrammingLang}}であり、構文はその名前にもある通り{LangsGroupOfC}({SampleLang1}、{SampleLang2}や{SampleLang3}など)の影響があるが、構文以外の言語機能などについては{DevelopersLastName}が以前の所属である{DevelopersOffice}で設計した{SampleLang4}からの影響がある。";
// 結果 -> C#(シーシャープ)は、アンダース・ヘルスバーグが設計したプログラミング言語であり、構文はその名前にもある通りC系言語(C言語、C++やJavaなど)の影響があるが、構文以外の言語機能などについてはヘルスバーグが以前の所属であるボーランドで設計したDelphiからの影響がある。
のように位置と値をセットで記述できるので、string.Format
に比べて読みやすいと思う。
プレースホルダに対して値を設定していない場合はコンパイルエラーとして検出してくれるので便利だ。
const string favoriteName = "リンゴ";
const string favoritePoint = "甘い";
string formattedText = $"{favoriteName}の好きなところは、{}ところです。"; // パラメータに favoritePoint を指定するのを忘れている
// 結果 -> コンパイルエラー
CS1733:式が必要です。
余談
文字列補間とはあまり関係ない余談だが、先日新人さんが
DateTime Nov042020 = new DateTime(2020, 11, 4);
string formattedText = Nov042020.ToString("今日はyyyy/M/dです。");
// 結果 -> 今日は2020/11/4です。
というコードを書いてきて頭を抱えたので、これはちょっとしっかり教えた方がいいかなと思ったのだった。まぁやりたいことはわかるんだけどね…。
日本語であればあまり関係ないかもしれないが、たとえば英語の利用者向けに同じメッセージを同じように書いたら
DateTime Nov042020 = new DateTime(2020, 11, 4);
string formattedText = Nov042020.ToString("It is d.M.yyyy today.");
// 結果 -> I午 i0 4.11.2020 午o4a20.
となってしまう。コード量は増えるが書式設定とメッセージは切り分けるべきだという流れでstring.Format
とstring-interpolation
を上記の例で教えた。
以上、余談でした。