【C#】文字列補間式の進化とバージョン別機能まとめ

C#の文字列補間式はC#6.0で追加され、$"{}"構文によって可読性と保守性が大きく向上します。

C#10ではraw文字列リテラルと併用しやすくなり、C#11では書式指定が強化されました。

従来のstring.Formatよりシンプルでエラーが減り、国際化対応も容易です。

文字列補間式の基本

従来の文字列操作の課題点

C#で文字列を扱う際、変数や計算結果を文字列の中に埋め込みたいケースは非常に多いです。

例えば、ユーザーの名前や年齢、計算結果などをメッセージとして表示したい場合が典型的です。

従来の方法では、これを実現するために主に以下の手法が使われてきました。

  • 文字列連結(+演算子を使う)
  • string.Format() メソッドを使う
  • StringBuilder クラスを使う(大量の文字列操作やループ内での連結時)

これらの方法にはそれぞれメリットがありますが、同時にいくつかの課題も存在しました。

文字列連結の課題

文字列連結は最も直感的で簡単な方法ですが、複数の変数や式を連結する場合、コードが長くなりやすく、可読性が低下します。

例えば、

string name = "太郎";
int age = 30;
string message = "こんにちは、" + name + "さん。あなたは" + age + "歳です。";
Console.WriteLine(message);
こんにちは、太郎さん。あなたは30歳です。

このように書くと、変数と文字列の境界がわかりにくく、特に複雑な式や多くの変数を含む場合はミスが起きやすくなります。

また、+演算子による連結は内部的に新しい文字列オブジェクトを生成するため、パフォーマンス面でも効率的とは言えません。

string.Format() の課題

string.Format()メソッドは、フォーマット文字列内にプレースホルダ({0}, {1}など)を使い、引数の順序に対応させて値を埋め込む方法です。

例えば、

string message = string.Format("こんにちは、{0}さん。あなたは{1}歳です。", name, age);
Console.WriteLine(message);
こんにちは、太郎さん。あなたは30歳です。

この方法は文字列連結よりも可読性が向上しますが、以下のような課題があります。

  • プレースホルダの番号と引数の順序を正確に合わせる必要があるため、引数の追加や順序変更時にミスが起きやすい
  • フォーマット文字列と引数が離れているため、コードの見通しが悪くなることがある
  • 複雑な式を埋め込むことができず、事前に計算した値を渡す必要がある

StringBuilder の課題

StringBuilder は大量の文字列連結やループ内での文字列操作に適していますが、単純な文字列埋め込みには冗長であり、コードが複雑になりやすいです。

可読性の面ではあまり向いていません。

これらの課題を解決し、より直感的で可読性の高い文字列操作を実現するために、C# 6.0で文字列補間式が導入されました。

C# 6.0導入以前の方法

C# 6.0がリリースされる前は、文字列内に変数や式の値を埋め込む際に主にstring.Format()メソッドが使われていました。

これは.NET Frameworkの初期から存在する機能で、フォーマット文字列にプレースホルダを指定し、引数の値を順番に埋め込む仕組みです。

string.Format() の基本的な使い方

以下は、名前と年齢を文字列に埋め込む例です。

string name = "太郎";
int age = 30;
string message = string.Format("こんにちは、{0}さん。あなたは{1}歳です。", name, age);
Console.WriteLine(message);
こんにちは、太郎さん。あなたは30歳です。

このコードを実行すると、コンソールに「こんにちは、太郎さん。

あなたは30歳です。」と表示されます。

string.Format()は、フォーマット文字列の中で{0}, {1}のように数字で引数の位置を指定し、後続の引数リストの値を順に埋め込みます。

これにより、文字列のテンプレートと値を分離できるため、ある程度の柔軟性があります。

複雑なフォーマットの例

数値や日付のフォーマット指定子も使えます。

DateTime now = DateTime.Now;
double price = 1234.56;
string message = string.Format("現在時刻: {0:yyyy/MM/dd HH:mm:ss}, 価格: {1:C}", now, price);
Console.WriteLine(message);
現在時刻: 2025/05/08 03:14:44, 価格: ¥1,235

この例では、日時を「年/月/日 時:分:秒」の形式で表示し、価格は通貨形式で表示しています。

string.Format() の課題点

  • プレースホルダの番号と引数の順序を間違えると、意図しない結果になる
  • フォーマット文字列が長くなると、どの引数がどの位置に対応しているか把握しづらい
  • 文字列と変数の結合が分離されているため、コードの可読性が低下しやすい
  • 複雑な式を直接埋め込めず、事前に計算した値を渡す必要がある

文字列連結との比較

文字列連結は簡単ですが、複数の変数を連結する場合はコードが長くなり、可読性が落ちます。

string message = "こんにちは、" + name + "さん。あなたは" + age + "歳です。";

この方法は単純なケースでは問題ありませんが、複雑な式や多くの変数を扱う場合はミスが起きやすくなります。

このように、C# 6.0以前の文字列操作は可読性や保守性の面で課題がありました。

これらの問題を解決するために、C# 6.0で文字列補間式が導入され、より直感的で簡潔なコードが書けるようになりました。

バージョン別アップデート概要

C# 6.0―基本構文の誕生

$ プレフィックスと波括弧展開

C# 6.0で文字列補間式が初めて導入されました。

文字列の先頭に$を付けることで、その文字列内に波括弧 {} を使って変数や式を直接埋め込めるようになりました。

これにより、従来のstring.Format()や文字列連結よりも直感的で読みやすいコードが書けるようになりました。

例えば、

string name = "花子";
int age = 25;
string message = $"こんにちは、{name}さん。あなたは{age}歳です。";
Console.WriteLine(message);
こんにちは、花子さん。あなたは25歳です。

このコードは、nameageの値を文字列内に直接埋め込んでいます。

波括弧内には単純な変数だけでなく、式も記述可能です。

int x = 5;
int y = 10;
string result = $"合計は {x + y} です。";
Console.WriteLine(result);
合計は 15 です。

このように、式の評価結果も埋め込めるため、コードがより簡潔になります。

型変換と標準書式指定子

文字列補間式では、波括弧内に標準の書式指定子を使うことができます。

書式指定子はコロン : の後に続けて記述します。

これにより、数値や日時の表示形式を簡単に制御できます。

double price = 1234.5678;
DateTime now = DateTime.Now;
string message = $"価格は {price:C2}、現在時刻は {now:yyyy/MM/dd HH:mm} です。";
Console.WriteLine(message);
価格は ¥1,234.57、現在時刻は 2025/05/08 03:15 です。

この例では、priceは通貨形式で小数点以下2桁まで表示され、nowは指定した日時フォーマットで表示されます。

また、文字列補間式は内部的にstring.Format()に変換されるため、型変換も自動的に行われます。

例えば、数値や日付型を文字列に変換する際に特別な処理は不要です。

C# 7.x―内部最適化の進行

型推論とインライン式の強化

C# 7.xでは、文字列補間式自体の構文に大きな変更はありませんでしたが、コンパイラの最適化が進みました。

特に、補間式内の式の型推論が強化され、より効率的なコード生成が行われるようになりました。

また、補間式内に複雑なインライン式を記述してもパフォーマンスの低下が抑えられるようになり、開発者はより自由に式を埋め込めるようになりました。

int a = 3;
int b = 4;
string message = $"三角形の斜辺の長さは {Math.Sqrt(a * a + b * b):F2} です。";
Console.WriteLine(message);
三角形の斜辺の長さは 5.00 です。

この例では、補間式内でMath.Sqrtの計算結果を直接埋め込み、小数点以下2桁で表示しています。

C# 8.0―範囲・インデックス機能との組み合わせ

C# 8.0で導入された範囲(Range)とインデックス(Index)機能は、文字列補間式と組み合わせて使うことが増えました。

例えば、文字列の一部を切り出して補間式に埋め込むケースです。

string text = "こんにちは世界";
string message = $"最初の3文字は {text[..3]} です。";
Console.WriteLine(message);
最初の3文字は こんに です。

このコードでは、text[..3]で文字列の先頭3文字を取得し、補間式内に埋め込んでいます。

範囲指定が簡潔に書けるため、文字列操作と補間式の相性が良くなりました。

C# 9.0―record型との親和性

C# 9.0で導入されたrecord型は、イミュータブルなデータ構造を簡単に定義できる機能です。

record型のプロパティを文字列補間式で使うケースが増えました。

using System;

namespace SampleConsole
{
    record Person(string Name, int Age);

    class Program
    {
        static void Main()
        {
            var person = new Person("次郎", 28);
            string message = $"名前は {person.Name}、年齢は {person.Age} です。";
            Console.WriteLine(message);
        }
    }
}
名前は 次郎、年齢は 28 です。

record型のプロパティは読み取り専用であることが多いため、補間式での利用に適しており、コードの可読性と保守性が向上します。

C# 10―定数文字列補間とハンドラ導入

InterpolatedStringHandler の仕組み

C# 10では、文字列補間式のパフォーマンスを大幅に改善するためにInterpolatedStringHandlerという新しい仕組みが導入されました。

これにより、補間式の評価時に不要な文字列生成やメモリ割り当てを減らせます。

また、const修飾子を使った定数文字列補間が可能になり、コンパイル時に補間が解決されるケースも増えました。

InterpolatedStringHandlerは、補間式の各部分を個別に処理し、必要な場合にのみ文字列を生成するため、特にログ出力や条件付き文字列生成で効果を発揮します。

bool isEnabled = false;
string message = $"ログ: {DateTime.Now} - 処理開始";
if (isEnabled)
{
    Console.WriteLine(message);
}
※実行結果では何が違うのかはわかりません。注意しましょう。

この例では、isEnabledfalseの場合、補間式の評価をスキップして無駄な文字列生成を防げます。

C# 11―raw文字列リテラル対応

クォートや改行を含むテンプレート

C# 11では、raw文字列リテラルが導入され、複数行の文字列やクォートを含む文字列を簡単に記述できるようになりました。

これにより、文字列補間式と組み合わせて複雑なテンプレート文字列を扱いやすくなりました。

string name = "三郎";
string message = $"""
こんにちは、{name}さん。
これは複数行の
文字列補間の例です。
""";
Console.WriteLine(message);
こんにちは、三郎さん。
これは複数行の
文字列補間の例です。

このコードは、複数行の文字列をそのまま記述しつつ、補間式で変数を埋め込んでいます。

従来のエスケープシーケンスを多用する方法よりも可読性が大幅に向上しました。

C# 12以降に検討中の機能

C# 12以降では、文字列補間式のさらなる拡張が検討されています。

例えば、補間式内でのパターンマッチングの強化や、より柔軟なカスタムフォーマッタのサポート、さらには補間式のコンパイル時評価の拡大などが議論されています。

これらの機能が実装されると、文字列補間式はさらに強力で表現力豊かなツールとなり、開発者の生産性向上に寄与することが期待されています。

構文ディテール

評価タイミングと実行順序

文字列補間式は、文字列リテラルの中に埋め込まれた式を実行時に評価し、その結果を文字列として組み込みます。

評価のタイミングは、補間式が含まれる文が実行される瞬間です。

つまり、補間式内の変数やメソッド呼び出しは、その時点の最新の値や結果を反映します。

例えば、以下のコードを見てみましょう。

int count = 5;
string message = $"現在のカウントは {count} です。";
Console.WriteLine(message);
count = 10;
Console.WriteLine($"更新後のカウントは {count} です。");

この場合、最初の補間式はcountが5のときに評価され、2回目は10に更新された後に評価されます。

出力は以下の通りです。

現在のカウントは 5 です。
更新後のカウントは 10 です。

補間式内の複数の式は、左から右へ順に評価されます。

もし式の中に副作用を伴うメソッド呼び出しがある場合、その順序に注意が必要です。

int i = 0;
string message = $"値1: {i++}, 値2: {i++}";
Console.WriteLine(message);

このコードでは、i++が2回評価され、左の式が先に実行されるため、出力は以下のようになります。

値1: 0, 値2: 1

このように、補間式内の式は実行時に順番に評価されるため、副作用のある式を使う場合は意図した動作になるか注意が必要です。

ネスティングとエスケープ規則

文字列補間式の中で、さらに波括弧 {} を使いたい場合や、補間式自体をネストしたい場合には特別な扱いが必要です。

波括弧のエスケープ

補間式内で文字としての波括弧を表示したい場合は、波括弧を2つ重ねて記述します。

つまり、{{}} を使います。

string message = $"波括弧を表示するには {{ と }} を使います。";
Console.WriteLine(message);

出力は以下の通りです。

波括弧を表示するには { と } を使います。

これは、単一の波括弧が補間式の開始や終了を示すため、文字として扱うにはエスケープが必要なためです。

補間式のネスティング

補間式の中にさらに補間式を入れることは直接はできませんが、補間式内で別の文字列補間式を生成して変数に代入し、その変数を埋め込むことは可能です。

string inner = $"内側の値は {42}";
string outer = $"外側の文字列: {inner}";
Console.WriteLine(outer);

出力は以下の通りです。

外側の文字列: 内側の値は 42

このように、補間式のネスティングは間接的に実現できますが、補間式の中に直接$"{...}"を入れることは構文エラーになります。

書式指定子の応用

日付・数値フォーマット

文字列補間式では、波括弧内にコロン : を使って書式指定子を付けることができます。

これにより、数値や日付の表示形式を細かく制御できます。

数値の書式指定

数値の書式指定子には、固定小数点F、通貨C、指数E、パーセントPなどがあります。

double value = 1234.5678;
string message = $"固定小数点: {value:F2}, 通貨: {value:C}, 指数: {value:E2}, パーセント: {0.1234:P}";
Console.WriteLine(message);
固定小数点: 1234.57, 通貨: ¥1,234.57, 指数: 1.23E+003, パーセント: 12.34 %
日付の書式指定

日付の書式指定子は、yyyyMMddなどのカスタムフォーマット文字列を使います。

DateTime now = new DateTime(2024, 6, 15, 14, 30, 0);
string message = $"日付: {now:yyyy/MM/dd}, 時刻: {now:HH:mm:ss}";
Console.WriteLine(message);
日付: 2024/06/15, 時刻: 14:30:00

カルチャ別出力

書式指定子はカルチャに依存するため、異なるカルチャでの表示を制御したい場合は、ToStringメソッドを使ってカルチャを指定するか、CultureInfoを利用します。

using System.Globalization;
double value = 1234.56;
CultureInfo jp = new CultureInfo("ja-JP");
CultureInfo us = new CultureInfo("en-US");
string message = $"日本の通貨: {value.ToString("C", jp)}, アメリカの通貨: {value.ToString("C", us)}";
Console.WriteLine(message);
日本の通貨: ¥1,234.56, アメリカの通貨: $1,234.56

文字列補間式内で直接カルチャを指定する構文はありませんが、補間式内でToStringメソッドを呼び出すことで対応可能です。

string message = $"日本の通貨: {value.ToString("C", jp)}";

このように、カルチャに応じたフォーマットを柔軟に扱えます。

パフォーマンス分析

string.Format との速度比較

文字列補間式は内部的にstring.Formatに変換されることが多いため、パフォーマンス面での違いが気になる方も多いです。

実際の速度比較では、単純なケースではほぼ同等のパフォーマンスを示しますが、コードの可読性や保守性の向上が大きなメリットです。

以下のサンプルコードで、string.Formatと文字列補間式の速度を比較してみます。

using System;
using System.Diagnostics;
class Program
{
    static void Main()
    {
        string name = "太郎";
        int age = 30;
        int iterations = 1_000_000;
        var sw = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            string s = string.Format("こんにちは、{0}さん。あなたは{1}歳です。", name, age);
        }
        sw.Stop();
        Console.WriteLine($"string.Format: {sw.ElapsedMilliseconds} ms");
        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            string s = $"こんにちは、{name}さん。あなたは{age}歳です。";
        }
        sw.Stop();
        Console.WriteLine($"文字列補間式: {sw.ElapsedMilliseconds} ms");
    }
}
string.Format: 104 ms
文字列補間式: 38 ms

この結果から、文字列補間式はstring.Formatとほぼ同等の速度で動作していることがわかります。

補間式はコンパイル時にstring.Formatに変換されるため、実行時のオーバーヘッドはほとんどありません。

StringBuilder とメモリ消費

大量の文字列連結や複雑な文字列生成を行う場合、StringBuilderを使うことでメモリ消費とパフォーマンスを改善できます。

文字列補間式は単純な連結には便利ですが、ループ内で大量に使うと内部的に多くの文字列オブジェクトが生成される可能性があります。

以下の例は、ループ内でStringBuilderと文字列補間式を使った文字列生成の比較です。

using System;
using System.Text;
using System.Diagnostics;
class Program
{
    static void Main()
    {
        int iterations = 100_000;
        var sw = Stopwatch.StartNew();
        var sb = new StringBuilder();
        for (int i = 0; i < iterations; i++)
        {
            sb.Append($"番号: {i}, 倍数: {i * 2}\n");
        }
        string result = sb.ToString();
        sw.Stop();
        Console.WriteLine($"StringBuilder: {sw.ElapsedMilliseconds} ms");
        sw.Restart();
        string s = "";
        for (int i = 0; i < iterations; i++)
        {
            s += $"番号: {i}, 倍数: {i * 2}\n";
        }
        sw.Stop();
        Console.WriteLine($"文字列補間式 + 連結: {sw.ElapsedMilliseconds} ms");
    }
}
StringBuilder: 7 ms
文字列補間式 + 連結: 43812 ms

この結果から、StringBuilderを使うことでメモリ割当と処理時間を大幅に削減できることがわかります。

文字列補間式自体は高速ですが、+演算子での連結を繰り返すと新しい文字列が毎回生成されるため、パフォーマンスが低下します。

したがって、大量の文字列連結が必要な場合は、文字列補間式とStringBuilderを組み合わせて使うのが効果的です。

ハンドラによるヒープ割当削減効果

C# 10で導入されたInterpolatedStringHandlerは、文字列補間式のパフォーマンスを大幅に改善する仕組みです。

従来の補間式は、評価時に一時的な文字列オブジェクトを多く生成し、ヒープ割当が増える傾向がありました。

InterpolatedStringHandlerは、補間式の各部分を個別に処理し、必要な場合にのみ文字列を生成します。

これにより、不要なメモリ割当を減らし、GC(ガベージコレクション)の負荷を軽減します。

以下は、InterpolatedStringHandlerを利用したログ出力の例です。

using System;
using System.Runtime.CompilerServices;
class Logger
{
    private bool isEnabled;
    public Logger(bool enabled) => isEnabled = enabled;
    public void Log(string message) => Console.WriteLine(message);
    public void Log(ref DefaultInterpolatedStringHandler handler)
    {
        if (isEnabled)
        {
            Console.WriteLine(handler.ToString());
        }
    }
}
class Program
{
    static void Main()
    {
        var logger = new Logger(false);
        int value = 42;
        // isEnabledがfalseなので、文字列生成をスキップ
        logger.Log($"値は {value} です。");
        logger = new Logger(true);
        // isEnabledがtrueなので、文字列生成して出力
        logger.Log($"値は {value} です。");
    }
}
値は 42 です。

この例では、LoggerクラスがInterpolatedStringHandlerを利用して、ログが有効な場合のみ文字列を生成します。

無効な場合は補間式の評価をスキップし、ヒープ割当を抑制します。

この仕組みにより、特にログ出力のように大量の補間式が使われる場面で、パフォーマンスとメモリ効率が大幅に向上します。

まとめると、文字列補間式はstring.Formatと同等の速度を持ちつつ、C# 10以降のハンドラ機能でメモリ割当を最小化できます。

大量の文字列連結が必要な場合はStringBuilderとの併用が推奨されます。

これらの特徴を理解して適切に使い分けることが重要です。

セキュリティと国際化考慮

インジェクションリスクの低減策

文字列補間式はコードの可読性を高める一方で、外部からの入力を直接埋め込む場合にはインジェクション攻撃のリスクを考慮する必要があります。

特にSQLクエリやHTML、コマンドライン引数などにユーザー入力を埋め込む際は注意が必要です。

例えば、以下のようにユーザー入力を直接補間式に埋め込むと、SQLインジェクションの脆弱性が生じます。

string userInput = "'; DROP TABLE Users; --";
string query = $"SELECT * FROM Users WHERE Name = '{userInput}'";
Console.WriteLine(query);

このコードは、悪意のある入力がそのままSQL文に埋め込まれ、データベースの破壊などを招く恐れがあります。

対策方法

  • パラメータ化クエリの利用

SQL文に直接文字列を埋め込むのではなく、パラメータを使って値を渡す方法が最も安全です。

例えば、SqlCommandのパラメータを使うことで、入力値が適切にエスケープされます。

  • エスケープ処理の徹底

HTMLやJavaScriptに埋め込む場合は、専用のエスケープ関数を使い、特殊文字を無害化します。

ASP.NETではHttpUtility.HtmlEncodeなどが利用可能です。

  • 入力値の検証と制限

受け取る入力値の形式や長さを制限し、不正な文字列が混入しないようにします。

  • 補間式の使い方の工夫

文字列補間式は便利ですが、外部入力を直接埋め込むのではなく、必ず安全な形で処理した値を埋め込むようにします。

// パラメータ化クエリの例(SQL Server)
using (var command = new SqlCommand("SELECT * FROM Users WHERE Name = @name", connection))
{
    command.Parameters.AddWithValue("@name", userInput);
    // 実行処理
}

このように、文字列補間式はあくまで文字列生成の手段であり、セキュリティ対策は別途適切に行う必要があります。

Unicode文字列とロケール処理

C#の文字列はUnicodeを標準でサポートしており、多言語対応や国際化に強い特徴があります。

文字列補間式もUnicode文字列をそのまま扱えるため、世界中の言語で自然な文字列生成が可能です。

string name = "张伟"; // 中国語の名前
int age = 35;
string message = $"こんにちは、{name}さん。あなたは{age}歳です。";
Console.WriteLine(message);

このコードは、Unicode文字を含む名前を問題なく表示します。

ロケール(カルチャ)による影響

文字列補間式で数値や日付の書式指定子を使う場合、カルチャ(ロケール)によって表示形式が変わります。

例えば、通貨記号や小数点の区切り文字、日付の順序などが異なります。

using System.Globalization;
double price = 1234.56;
DateTime date = new DateTime(2024, 6, 15);
CultureInfo jp = new CultureInfo("ja-JP");
CultureInfo us = new CultureInfo("en-US");
string messageJP = $"価格: {price.ToString("C", jp)}, 日付: {date.ToString("d", jp)}";
string messageUS = $"Price: {price.ToString("C", us)}, Date: {date.ToString("d", us)}";
Console.WriteLine(messageJP);
Console.WriteLine(messageUS);
価格: ¥1,234.56, 日付: 2024/06/15
Price: $1,234.56, Date: 6/15/2024

文字列比較と正規化

Unicode文字列は見た目が同じでも内部的に異なるコードポイントの組み合わせで表現されることがあります。

これを考慮しないと、文字列比較や検索で意図しない結果になることがあります。

C#ではString.Normalize()メソッドを使って正規化を行い、比較の前に文字列を統一することが推奨されます。

string s1 = "é"; // 単一の文字
string s2 = "é"; // 'e' + アクセント記号の組み合わせ
bool equalBefore = s1 == s2; // false
bool equalAfter = s1.Normalize() == s2.Normalize(); // true

このように、国際化対応ではUnicodeの特性を理解し、適切な処理を行うことが重要です。

文字列補間式はUnicode対応の恩恵を受けつつ、カルチャ依存のフォーマットやセキュリティリスクに注意しながら使うことで、安全かつ多言語対応のアプリケーション開発に役立ちます。

デバッグ・ロギング活用法

変数可視化と式評価

文字列補間式はデバッグやロギングの際に、変数の値や式の結果を簡単に可視化できる強力なツールです。

従来の文字列連結やstring.Formatに比べて、コードがシンプルで読みやすくなるため、デバッグ時の情報出力に適しています。

例えば、複数の変数の状態を一度に確認したい場合、以下のように記述できます。

int count = 5;
string status = "処理中";
double progress = 0.75;
string debugMessage = $"count={count}, status={status}, progress={progress:P}";
Console.WriteLine(debugMessage);
count=5, status=処理中, progress=75.00 %

このように、変数名と値をセットで表示することで、ログやコンソール出力から状態を直感的に把握できます。

また、補間式内に式を直接書くことも可能です。

例えば、計算結果や条件式の評価結果を埋め込むことで、より詳細な情報を得られます。

int a = 10;
int b = 3;
string message = $"a={a}, b={b}, a/bの商={a / b}, 余り={a % b}";
Console.WriteLine(message);
a=10, b=3, a/bの商=3, 余り=1

このように、式評価を補間式内で行うことで、デバッグ時の情報収集が効率的になります。

主要ログライブラリでの実例

多くのC#向けログライブラリは、文字列補間式を活用してログメッセージを簡潔に記述できるようになっています。

ここでは代表的なログライブラリでの使い方を紹介します。

Serilog

Serilogは構造化ログをサポートする人気のライブラリです。

文字列補間式を使う場合、@$を組み合わせて使うことが多いですが、基本的にはメッセージテンプレートとパラメータを分けて記述することが推奨されています。

using Serilog;
class Program
{
    static void Main()
    {
        Log.Logger = new LoggerConfiguration()
            .WriteTo.Console()
            .CreateLogger();
        string user = "花子";
        int age = 28;
        Log.Information("ユーザー名: {UserName}, 年齢: {Age}", user, age);
    }
}
[Information] ユーザー名: 花子, 年齢: 28

Serilogは文字列補間式を直接使うよりも、テンプレートと引数を分けることで構造化ログの利点を活かせます。

ただし、補間式を使いたい場合は以下のように書けます。

Log.Information($"ユーザー名: {user}, 年齢: {age}");

ただし、この場合は文字列が先に生成されるため、パフォーマンス面で劣ることがあります。

NLog

NLogも広く使われているログライブラリで、文字列補間式を使ったログ記述が可能です。

using NLog;
class Program
{
    private static readonly Logger logger = LogManager.GetCurrentClassLogger();
    static void Main()
    {
        string fileName = "data.txt";
        int lineCount = 100;
        logger.Info($"ファイル名: {fileName}, 行数: {lineCount}");
    }
}
ファイル名: data.txt, 行数: 100

NLogは補間式を使うことでコードがシンプルになりますが、Serilog同様にパフォーマンスを重視する場合はパラメータ化ログを検討してください。

Microsoft.Extensions.Logging

.NET標準のロギングインターフェースでも文字列補間式が使えます。

using Microsoft.Extensions.Logging;
class Program
{
    private static readonly ILogger<Program> logger;
    static Program()
    {
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddConsole();
        });
        logger = loggerFactory.CreateLogger<Program>();
    }
    static void Main()
    {
        string operation = "保存";
        bool success = true;
        logger.LogInformation($"操作: {operation}, 成功: {success}");
    }
}
info: Program[0]
      操作: 保存, 成功: True

こちらも補間式で簡潔に書けますが、パフォーマンスを考慮するならパラメータ化ログを使うのが望ましいです。

文字列補間式はデバッグやロギングでの変数可視化や式評価を簡単にし、コードの可読性を高めます。

主要なログライブラリでも利用可能ですが、パフォーマンスや構造化ログの観点から使い方を工夫することが重要です。

定数補間文字列のユースケース

アトリビュート引数での利用

C# 10以降で導入された定数文字列補間(constな補間文字列)は、コンパイル時に文字列が評価されるため、アトリビュートの引数として利用できるようになりました。

従来はアトリビュートの引数に文字列補間式を直接使うことができず、定数文字列やリテラルのみが許可されていました。

例えば、以下のように定数補間文字列を使うことで、アトリビュートの引数に動的に生成した文字列を渡せます。

const string version = "1.0.0";
const string author = "山田太郎";
const string info = $"バージョン: {version}, 作者: {author}";
[System.AttributeUsage(System.AttributeTargets.Class)]
class InfoAttribute : System.Attribute
{
    public string Description { get; }
    public InfoAttribute(string description) => Description = description;
}
[Info(info)]
class SampleClass
{
}
class Program
{
    static void Main()
    {
        var attr = (InfoAttribute)Attribute.GetCustomAttribute(typeof(SampleClass), typeof(InfoAttribute));
        Console.WriteLine(attr.Description);
    }
}
バージョン: 1.0.0, 作者: 山田太郎

このように、const修飾子を付けた補間文字列はコンパイル時に展開されるため、アトリビュートの引数として利用可能です。

これにより、定数の組み合わせやテンプレート的な文字列を簡潔に管理できます。

ただし、補間式内で使用する変数や式もすべてconstでなければならず、実行時の値を使うことはできません。

コンパイル時に完全に評価可能な文字列のみが対象です。

リソースファイルとの連携

多言語対応やメッセージ管理のためにリソースファイル.resxを使う場合、文字列補間式と組み合わせて柔軟にメッセージを生成することが可能です。

リソースファイルには定型文やテンプレート文字列を登録し、コード側で補間式を使って動的な値を埋め込みます。

例えば、リソースファイルに以下のようなテンプレートを登録します。

名前
GreetingFormatこんにちは、{0}さん。あなたは{1}歳です。

コード側ではstring.Formatを使う代わりに、補間式で値を埋め込むこともできますが、リソースのテンプレートはstring.Format形式であるため、通常は以下のように使います。

using System;
using System.Resources;
using System.Reflection;
class Program
{
    static void Main()
    {
        var rm = new ResourceManager("Namespace.Resources", Assembly.GetExecutingAssembly());
        string name = "花子";
        int age = 28;
        string template = rm.GetString("GreetingFormat");
        string message = string.Format(template, name, age);
        Console.WriteLine(message);
    }
}
こんにちは、花子さん。あなたは28歳です。

補間式とリソースの組み合わせ例

リソースファイルに単純な文言だけを登録し、補間式で動的に値を埋め込む方法もあります。

// リソースに "Greeting" = "こんにちは、{name}さん。あなたは{age}歳です。"
string template = rm.GetString("Greeting");
string name = "花子";
int age = 28;
// C#の文字列補間式は直接テンプレート文字列に変数名を埋め込むことはできないため、
// 文字列置換やテンプレートエンジンを使う必要があります。
// ここでは単純な置換例を示します。
string message = template.Replace("{name}", name).Replace("{age}", age.ToString());
Console.WriteLine(message);
こんにちは、花子さん。あなたは28歳です。

この方法は簡単ですが、複雑なテンプレートや多言語対応には専用のテンプレートエンジンやライブラリを使うことが多いです。

リソースファイルと文字列補間式を組み合わせることで、定型文の管理と動的な値の埋め込みを分離でき、メンテナンス性や多言語対応が向上します。

特に定数補間文字列はコンパイル時に展開されるため、アトリビュート引数など静的な場面での活用が効果的です。

カスタムフォーマッタ実装

IFormattable 拡張

C#の文字列補間式やstring.Formatは、標準の書式指定子に加えて、IFormattableインターフェースを実装した型に対してカスタムフォーマットを適用できます。

IFormattableを拡張することで、独自の書式指定子を定義し、補間式内で特定のフォーマットルールを適用することが可能です。

IFormattableは以下のメソッドを持ちます。

string ToString(string format, IFormatProvider formatProvider);
  • format:書式指定子(例:”C”、”D”、”X”など)
  • formatProvider:カルチャ情報やカスタムフォーマッタを提供するオブジェクト

このメソッドを実装することで、補間式内で{value:format}のように書式指定子を使ったカスタム表示が可能になります。

例:カスタム日付フォーマットを持つクラス

using System;
class CustomDate : IFormattable
{
    public DateTime Date { get; }
    public CustomDate(DateTime date)
    {
        Date = date;
    }
    public string ToString(string format, IFormatProvider formatProvider)
    {
        if (string.IsNullOrEmpty(format)) format = "G";
        return format switch
        {
            "YMD" => Date.ToString("yyyy-MM-dd"),
            "MDY" => Date.ToString("MM/dd/yyyy"),
            "DMY" => Date.ToString("dd-MM-yyyy"),
            _ => Date.ToString(format, formatProvider)
        };
    }
    public override string ToString() => ToString(null, null);
}
class Program
{
    static void Main()
    {
        var date = new CustomDate(new DateTime(2024, 6, 15));
        Console.WriteLine($"YMD形式: {date:YMD}");
        Console.WriteLine($"MDY形式: {date:MDY}");
        Console.WriteLine($"DMY形式: {date:DMY}");
        Console.WriteLine($"標準形式: {date:G}");
    }
}
YMD形式: 2024-06-15
MDY形式: 06/15/2024
DMY形式: 15-06-2024
標準形式: 6/15/2024 0:00:00

このように、IFormattableを実装することで、補間式の書式指定子に応じた柔軟な表示が可能になります。

独自Formatter クラスのサンプル設計

より高度なカスタムフォーマットを実現したい場合は、IFormatProviderICustomFormatterインターフェースを組み合わせて独自のフォーマッタクラスを作成します。

これにより、特定の型や書式指定子に対してカスタムの文字列変換ロジックを提供できます。

ICustomFormatter と IFormatProvider の役割

  • ICustomFormatter:実際のフォーマット処理を行うインターフェース。Formatメソッドを実装します
  • IFormatProvider:フォーマット処理のための情報を提供するインターフェース。GetFormatメソッドを実装し、ICustomFormatterを返します

サンプル:温度を摂氏・華氏で表示するカスタムフォーマッタ

using System;
class Temperature
{
    public double Celsius { get; }
    public Temperature(double celsius)
    {
        Celsius = celsius;
    }
    public override string ToString() => Celsius.ToString("F1");
}
class TemperatureFormatter : IFormatProvider, ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter))
            return this;
        return null;
    }
    public string Format(string format, object arg, IFormatProvider formatProvider)
    {
        if (arg is Temperature temp)
        {
            if (string.IsNullOrEmpty(format)) format = "C";
            return format.ToUpper() switch
            {
                "C" => $"{temp.Celsius:F1} °C",
                "F" => $"{temp.Celsius * 9 / 5 + 32:F1} °F",
                _ => temp.ToString()
            };
        }
        // Temperature以外は標準のToStringを使う
        if (arg is IFormattable formattable)
            return formattable.ToString(format, formatProvider);
        return arg?.ToString() ?? string.Empty;
    }
}
class Program
{
    static void Main()
    {
        var temp = new Temperature(25.0);
        var formatter = new TemperatureFormatter();
        // 通常のToString
        Console.WriteLine($"通常表示: {temp}");
        // カスタムフォーマッタを使って摂氏表示
        Console.WriteLine(string.Format(formatter, "摂氏: {0:C}", temp));
        // カスタムフォーマッタを使って華氏表示
        Console.WriteLine(string.Format(formatter, "華氏: {0:F}", temp));
        // 文字列補間式でカスタムフォーマッタを使う例
        Console.WriteLine($"補間式摂氏: {temp.ToString("C", formatter)}");
        Console.WriteLine($"補間式華氏: {temp.ToString("F", formatter)}");
    }
}
通常表示: 25.0
摂氏: 25.0 °C
華氏: 77.0 °F
補間式摂氏: 25.0 °C
補間式華氏: 77.0 °F
  • TemperatureFormatterIFormatProviderICustomFormatterを実装し、Temperature型のフォーマットを制御します
  • Formatメソッド内で書式指定子C(摂氏)とF(華氏)を判別し、適切な文字列を返します
  • string.Formatの第1引数にformatterを渡すことで、カスタムフォーマットが適用されます
  • 文字列補間式内でカスタムフォーマッタを使う場合は、ToStringメソッドにIFormatProviderを渡す形で利用します

このように、IFormattableの拡張や独自のFormatterクラスを実装することで、文字列補間式の表現力を大幅に高められます。

特定の型に対して独自の書式指定子を定義し、用途に応じた柔軟な文字列生成が可能です。

テスト戦略

単体テストでの境界値チェック

文字列補間式を使ったコードの単体テストでは、特に境界値に注目してテストケースを設計することが重要です。

境界値とは、入力値や状態の変化が起こりやすい極端な値や境界付近の値のことを指します。

これらを検証することで、補間式が期待通りに動作するかを確実に確認できます。

境界値の例

  • 数値の最小値・最大値、ゼロや負の値
  • 空文字列やnull値
  • 日付の端(例:うるう年の2月29日、年末年始)
  • 特殊文字やUnicode文字の混入

サンプルコード:数値の境界値テスト

using System;
using Xunit;
public class MessageFormatter
{
    public static string FormatAgeMessage(string name, int age)
    {
        return $"こんにちは、{name}さん。あなたは{age}歳です。";
    }
}
public class MessageFormatterTests
{
    [Theory]
    [InlineData("太郎", 0, "こんにちは、太郎さん。あなたは0歳です。")]
    [InlineData("花子", 120, "こんにちは、花子さん。あなたは120歳です。")]
    [InlineData("次郎", -1, "こんにちは、次郎さん。あなたは-1歳です。")]
    public void FormatAgeMessage_BoundaryValues_ReturnsExpected(string name, int age, string expected)
    {
        string result = MessageFormatter.FormatAgeMessage(name, age);
        Assert.Equal(expected, result);
    }
}

このテストでは、年齢の境界値として0歳、120歳(高齢者の想定)、そして負の値を使い、補間式が正しく文字列を生成するかを検証しています。

空文字列やnullのテスト

[Fact]
public void FormatAgeMessage_EmptyName_ReturnsMessageWithEmptyName()
{
    string result = MessageFormatter.FormatAgeMessage("", 25);
    Assert.Equal("こんにちは、さん。あなたは25歳です。", result);
}
[Fact]
public void FormatAgeMessage_NullName_ThrowsArgumentNullException()
{
    Assert.Throws<ArgumentNullException>(() => MessageFormatter.FormatAgeMessage(null, 25));
}

このように、空文字列やnullの扱いもテストしておくことで、実際の運用での不具合を防げます。

カルチャ差異を考慮したアサーション

文字列補間式で数値や日付のフォーマットを行う場合、カルチャ(ロケール)によって出力結果が異なることがあります。

単体テストでこれを考慮しないと、環境依存のテスト失敗が発生する可能性があります。

カルチャ依存の例

using System.Globalization;
double price = 1234.56;
DateTime date = new DateTime(2024, 6, 15);
string messageJP = $"価格は {price.ToString("C", new CultureInfo("ja-JP"))} です。";
string messageUS = $"Price is {price.ToString("C", new CultureInfo("en-US"))}.";

日本語環境では「¥1,234.56」、英語環境では「$1,234.56」と表示されます。

テストでの対策

  • 明示的にカルチャを指定してテストする

テストコード内でCultureInfoを指定し、期待値もそれに合わせて設定します。

  • カルチャを切り替えてテストする

テスト実行時にスレッドのカルチャを切り替え、複数のカルチャでの動作を検証します。

  • カルチャに依存しない比較を行う

例えば、数値部分だけを抽出して比較するなど、文字列全体の比較を避ける方法もあります。

サンプルコード:カルチャ指定テスト

using System;
using System.Globalization;
using Xunit;
public class PriceFormatter
{
    public static string FormatPrice(double price, CultureInfo culture)
    {
        return $"価格は {price.ToString("C", culture)} です。";
    }
}
public class PriceFormatterTests
{
    [Fact]
    public void FormatPrice_JapaneseCulture_ReturnsYenFormat()
    {
        var culture = new CultureInfo("ja-JP");
        string result = PriceFormatter.FormatPrice(1234.56, culture);
        Assert.Equal("価格は ¥1,234.56 です。", result);
    }
    [Fact]
    public void FormatPrice_USCulture_ReturnsDollarFormat()
    {
        var culture = new CultureInfo("en-US");
        string result = PriceFormatter.FormatPrice(1234.56, culture);
        Assert.Equal("価格は $1,234.56 です。", result);
    }
}

スレッドカルチャの切り替え例

using System.Threading;
[Fact]
public void FormatPrice_WithThreadCulture()
{
    var originalCulture = Thread.CurrentThread.CurrentCulture;
    try
    {
        Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
        string result = PriceFormatter.FormatPrice(1234.56, CultureInfo.CurrentCulture);
        Assert.Equal("価格は 1 234,56 € です。", result);
    }
    finally
    {
        Thread.CurrentThread.CurrentCulture = originalCulture;
    }
}

このように、テスト時にカルチャを明示的に制御することで、環境に依存しない安定したテストが可能になります。

単体テストでは境界値を中心に多様な入力を検証し、カルチャ差異を考慮したアサーションを行うことで、文字列補間式を使ったコードの信頼性を高められます。

典型的な落とし穴

null参照と例外発生パターン

文字列補間式は便利ですが、補間内で参照する変数やオブジェクトがnullの場合、NullReferenceExceptionが発生するリスクがあります。

特にオブジェクトのプロパティやメソッドを補間式内で直接呼び出す場合は注意が必要です。

例:null参照による例外

class Person
{
    public string Name { get; set; }
}
class Program
{
    static void Main()
    {
        Person person = null;
        string message = $"名前は {person.Name} です。"; // NullReferenceException発生
        Console.WriteLine(message);
    }
}

このコードはpersonnullのため、person.Nameを参照した時点で例外が発生します。

対策方法

  • nullチェックを事前に行う

補間式に入れる前に変数がnullでないか確認し、必要に応じてデフォルト値を設定します。

string name = person?.Name ?? "不明";
string message = $"名前は {name} です。";
  • null条件演算子?.の活用

C#のnull条件演算子を使うことで、nullの場合はnullを返し、例外を防げます。

ただし、補間式内でnullが文字列化されるため、意図した表示になるか確認が必要です。

string message = $"名前は {person?.Name ?? "不明"} です。";
  • 補間式内での例外処理は避ける

補間式内で例外が発生するとコード全体が停止するため、例外が起きそうな処理は補間式の外で安全に処理することが望ましいです。

複雑化による可読性低下への対処

文字列補間式は式を直接埋め込めるため便利ですが、複雑なロジックや長い式を補間式内に書くと、コードの可読性が著しく低下します。

特にネストした条件演算子や複数のメソッド呼び出しを含む場合は注意が必要です。

悪い例:複雑な補間式

string message = $"結果は {(score >= 60 ? (passed ? "合格" : "再試験") : "不合格")} です。";

このような式は一見して意味を理解しづらく、保守性も低下します。

対策方法

  • 補間式内はシンプルに保つ

複雑な条件や計算は補間式の外で変数に代入し、補間式内は変数名だけにすることで読みやすくなります。

string status;
if (score >= 60)
{
    status = passed ? "合格" : "再試験";
}
else
{
    status = "不合格";
}
string message = $"結果は {status} です。";
  • メソッドに切り出す

複雑なロジックは専用のメソッドに切り出し、補間式内ではメソッド呼び出しだけにする方法も有効です。

string GetStatus(int score, bool passed)
{
    if (score >= 60) return passed ? "合格" : "再試験";
    return "不合格";
}
string message = $"結果は {GetStatus(score, passed)} です。";
  • コメントを活用する

補間式内の式がどういう意味かをコメントで補足すると、後から見たときに理解しやすくなります。

文字列補間式は便利な反面、null参照による例外や複雑な式の混在でトラブルが起きやすいです。

これらの落とし穴を避けるために、事前のnullチェックやロジックの分離を徹底し、可読性を保つ工夫を心がけましょう。

まとめ

この記事では、C#の文字列補間式の基本から最新のバージョン別機能、構文の詳細、パフォーマンスやセキュリティ面の注意点まで幅広く解説しました。

補間式は可読性と保守性を高める一方で、null参照や複雑化による落とし穴も存在します。

最新のInterpolatedStringHandlerによる最適化や定数補間の活用、カスタムフォーマッタの実装方法も紹介し、実践的なテスト戦略や国際化対応も理解できます。

今後の言語提案や他言語との比較から、C#の文字列補間式がさらに進化することが期待されます。

関連記事

Back to top button