tag:crieit.net,2005:https://crieit.net/tags/%E6%96%87%E5%AD%97%E5%88%97%E8%A3%9C%E9%96%93/feed
「文字列補間」の記事 - Crieit
Crieitでタグ「文字列補間」に投稿された最近の記事
2021-01-04T21:09:12+09:00
https://crieit.net/tags/%E6%96%87%E5%AD%97%E5%88%97%E8%A3%9C%E9%96%93/feed
tag:crieit.net,2005:PublicArticle/16503
2021-01-04T21:06:50+09:00
2021-01-04T21:09:12+09:00
https://crieit.net/posts/C-string-interpolation
【C#】string-interpolation(文字列補間)の使い方とメリット
<h1 id="序論"><a href="#%E5%BA%8F%E8%AB%96">序論</a></h1>
<p>なんか最近仕事してて、自分が当たり前と思っていることでも、そうじゃない人もいるんだなぁと改めて思った。これは考えてみれば当然のことではあるんだけど。</p>
<p>先日C#のコードを書いていたら、それを見た人から「これ何?」と聞かれた。C#の<code>string-interpolation</code>(文字列補間)の書式が分からないらしかった。私は<code>string.Format</code>よりも文字列補間式を好むのだが、まぁ分からなければ難しいのだろうか?学習コストはそこまで高くないと個人的には思うし、こういうことがあるのでせっかくならまとめてみようと思った。</p>
<p>まぁそんな固く考えなくてもとりあえず書いちゃえって感じなので書きます。ついでに<code>string.Format</code>についてもおさらい。</p>
<h1 id="string.Format"><a href="#string.Format">string.Format</a></h1>
<p>string型には<code>Format</code>というメソッドがある。これは文字列の中に<code>{0}</code>などのプレースホルダを設定しておくと、引数をそこに入れてくれるというものだ。</p>
<pre><code class="csharp">const string name = "あぱしょに";
const int age = 27;
string text = "私の名前は{0}です。年齢は{1}歳です。";
string formattedText = string.Format(text, name, age);
// 結果 -> 私の名前はあぱしょにです。年齢は27歳です。
</code></pre>
<p>ちなみにパラメータ引数はobject型を取って内部で<code>ToString</code>しているので型は意識しなくても良い。params object[]を取るので型がバラバラでも構わない。</p>
<p>プレースホルダは同じインデックスを指定するとまとめて入れることもできる。</p>
<pre><code class="csharp">const string favoriteName = "リンゴ";
const string favoritePoint = "甘い";
string text = "私の好物は{0}です。{0}の好きなところは、{1}ところです。";
string formattedText = string.Format(text, favoriteName, favoritePoint);
// 結果 -> 私の好物はリンゴです。リンゴの好きなところは、甘いところです。
</code></pre>
<p>第2パラメータ以降で必要な数は、<strong>プレースホルダの数ではなくプレースホルダの種類(割り当てたインデックス)の数</strong>である。プレースホルダが2種類で合計3個なら、第2パラメータ以降は2つ指定する。</p>
<p>書式設定を指定することも出来る。プレースホルダのインデックスの後に<code>:</code>を挟んで書式設定を指定する。</p>
<pre><code class="csharp">DateTime Nov042020 = new DateTime(2020, 11, 4);
string text = "今日は{0:yyyy年MM月dd日}です。";
string formattedText = string.Format(text, Nov042020);
// 結果 -> 今日は2020年11月04日です。
</code></pre>
<h1 id="拡張メソッド化"><a href="#%E6%8B%A1%E5%BC%B5%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%8C%96">拡張メソッド化</a></h1>
<p>string.Formatをいちいち書くのは面倒なので、拡張メソッドを用意している人も多いと思う。</p>
<pre><code class="csharp">public static class StringExtension
{
public static string FormatEx(this string text, params object[] parameters) => string.Format(text, parameters);
}
</code></pre>
<p>たとえば上記のようなメソッドを用意しておけば、</p>
<pre><code class="csharp">DateTime Nov042020 = new DateTime(2020, 11, 4);
string text = "今日は{0:yyyy年MM月dd日}です。";
string formattedText = text.FormatEx(Nov042020);
// 結果 -> 今日は2020年11月04日です。
</code></pre>
<p>のように使える。こういう拡張メソッドを用意しておくとタイプ量も減るしテストもしやすいし可読性も上がると個人的には思う。</p>
<h1 id="string.Formatのイマイチな点"><a href="#string.Format%E3%81%AE%E3%82%A4%E3%83%9E%E3%82%A4%E3%83%81%E3%81%AA%E7%82%B9">string.Formatのイマイチな点</a></h1>
<p>2点ある。</p>
<ol>
<li>パラメータがどのプレースホルダに入るのか分かりづらい</li>
<li>コンパイルエラーを検出してくれない</li>
</ol>
<p>まずは1点目。プレースホルダが少なければ別に問題ないのだが、たとえば</p>
<pre><code class="csharp">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からの影響がある。
</code></pre>
<p>上記のような長ったらしい文字列の補間などは目で追う気にもならない。はっきりと言って苦痛である。(例文は<a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/C_Sharp">C#のwikipediaのページ</a>より拝借した)</p>
<p>こういうコードを書く方が問題だといえば当然そうなのだが、しかし現実問題として先人が書いたこういうコードを読むことが何度もあった。</p>
<p>こういうふうにプレースホルダとパラメータが多いと、対応するパラメータを探すのだけで一苦労であるし、上記では<code>{3}</code>が2回登場しているが、その度にパラメータをまた辿りなおすのも面倒である。</p>
<p>整形後のイメージがコメントで書いてあればまだマシだが、そこまで気が利くような人はこんな長ったらしい<code>Format</code>を書かないのである。</p>
<p>そして2点目、<code>string.Format</code>はコンパイルエラーを検知できない。</p>
<pre><code class="csharp">const string favoriteName = "リンゴ";
const string favoritePoint = "甘い";
string text = "{0}の好きなところは、{1}ところです。";
string formattedText = string.Format(text, favoriteName); // パラメータに favoritePoint を指定するのを忘れている
// 結果 -> 実行時エラー
</code></pre>
<p>上記のコードはプレースホルダに対してパラメータが不足しているため、下記の実行時エラーが発生する。</p>
<blockquote>
<p>System.FormatException: 'インデックス (0 ベース) は 0 以上で、引数リストのサイズよりも小さくなければなりません。'</p>
</blockquote>
<p>しかし、<code>string.Format</code>の構文としては正しい(第1引数がstring、第二引数がobject)ため、コンパイルエラーとして検知されない。つまり、実行してみるまでエラーになるか分らないのだ。</p>
<p>上記のような簡単なコードであれば原因も分かるが、「1.パラメータがどのプレースホルダに入るのか分かりづらい」の説明に例示したような複雑に入り組んだコードの場合は原因を探すモチベーションすら湧いてこないだろう。</p>
<p>上記の問題は、いずれも<code>string-interpolation</code>(文字列補間)を利用することで解消できる。</p>
<h1 id="string-interpolation(文字列補間)"><a href="#string-interpolation%28%E6%96%87%E5%AD%97%E5%88%97%E8%A3%9C%E9%96%93%29">string-interpolation(文字列補間)</a></h1>
<p>この機能は<a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history">C# 6.0から利用できるようになっている</a>。</p>
<p>利用するには、文字列の最初に<code>$</code>を付けて、プレースホルダは<code>{}</code>で囲んだ中に直接値(あるいは変数・定数など)を書き込む。</p>
<pre><code class="csharp">const string name = "あぱしょに";
const int age = 27;
string formattedText = $"私の名前は{name}です。年齢は{age}歳です。";
// 結果 -> 私の名前はあぱしょにです。年齢は27歳です。
</code></pre>
<p>書式設定を指定する時は、値の後に<code>:</code>を挟み指定することができる。</p>
<pre><code class="csharp">DateTime Nov042020 = new DateTime(2020, 11, 4);
string formattedText = $"今日は{Nov042020:yyyy年MM月dd日}です。";
// 結果 -> 今日は2020年11月04日です。
</code></pre>
<p>という具合だ。</p>
<p>また、プレースホルダが多い場合でも</p>
<pre><code class="csharp">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<span>}</span><span>}</span>であり、構文はその名前にもある通り{LangsGroupOfC}({SampleLang1}、{SampleLang2}や{SampleLang3}など)の影響があるが、構文以外の言語機能などについては{DevelopersLastName}が以前の所属である{DevelopersOffice}で設計した{SampleLang4}からの影響がある。";
// 結果 -> C#(シーシャープ)は、アンダース・ヘルスバーグが設計したプログラミング言語であり、構文はその名前にもある通りC系言語(C言語、C++やJavaなど)の影響があるが、構文以外の言語機能などについてはヘルスバーグが以前の所属であるボーランドで設計したDelphiからの影響がある。
</code></pre>
<p>のように位置と値をセットで記述できるので、<code>string.Format</code>に比べて読みやすいと思う。</p>
<p>プレースホルダに対して値を設定していない場合はコンパイルエラーとして検出してくれるので便利だ。</p>
<pre><code class="csharp">const string favoriteName = "リンゴ";
const string favoritePoint = "甘い";
string formattedText = $"{favoriteName}の好きなところは、{}ところです。"; // パラメータに favoritePoint を指定するのを忘れている
// 結果 -> コンパイルエラー
</code></pre>
<blockquote>
<p>CS1733:式が必要です。</p>
</blockquote>
<h1 id="余談"><a href="#%E4%BD%99%E8%AB%87">余談</a></h1>
<p>文字列補間とはあまり関係ない余談だが、先日新人さんが</p>
<pre><code class="csharp">DateTime Nov042020 = new DateTime(2020, 11, 4);
string formattedText = Nov042020.ToString("今日はyyyy/M/dです。");
// 結果 -> 今日は2020/11/4です。
</code></pre>
<p>というコードを書いてきて頭を抱えたので、これはちょっとしっかり教えた方がいいかなと思ったのだった。まぁやりたいことはわかるんだけどね…。</p>
<p>日本語であればあまり関係ないかもしれないが、たとえば英語の利用者向けに同じメッセージを同じように書いたら</p>
<pre><code class="csharp">DateTime Nov042020 = new DateTime(2020, 11, 4);
string formattedText = Nov042020.ToString("It is d.M.yyyy today.");
// 結果 -> I午 i0 4.11.2020 午o4a20.
</code></pre>
<p>となってしまう。コード量は増えるが書式設定とメッセージは切り分けるべきだという流れで<code>string.Format</code>と<code>string-interpolation</code>を上記の例で教えた。</p>
<p>以上、余談でした。</p>
あぱしょに