目次

キーワード

概要

Ver. 6

C# 6 で、補完文字列と、nameof 演算子(nameof operator)という、2つの文字列関連機能が追加されました。

また、C# 11 で、生文字列リテラルという構文が追加されました。

文字列補間

クラスのメンバーを整形して文字列化するには、.NETではstringFormatメソッドを使います。

var formatted = string.Format("({0}, {1})", x, y);

string.Format メソッドの利用例
string.Format メソッドの利用例

しかし、Formatメソッドには、以下のような面倒事がありました。

  • 頻出するわりに、string.Format という長めのタイピングが面倒

  • 値を埋め込みたい場所と、埋め込む値を渡す場所が離れて読みにくい

  • {0}とかの数と、渡す値の数が違っていても実行して見るまで気付かない

そこで、以下のような、Format用の専用構文が追加されました。

var formatted = $"({x}, {y})";

このような書き方を補間文字列(interpolated string)、もしくは、文字列補間(string interpolation)といいます。 文字列補間の結果は、単純に string.Format メソッドの呼び出しに置き替えられます。 例えば、最初の例は以下のコードと同じ意味なります。

var formatted = string.Format("({0}, {1})", x, y);

C# 10 でのパフォーマンス改善

Ver. 10

string.Format を使った実装ではどうしてもパフォーマンス上の改善が難しく、 C# 10.0 では別の型を使って結構複雑なコードに変換する最適化が入りました。 条件を満たす場合、

var formatted = $"({x}, {y})";

このコードは string.Format ではなく、以下のようなコードに展開されます。

DefaultInterpolatedStringHandler handler = new DefaultInterpolatedStringHandler(4, 2);
handler.AppendLiteral("(");
handler.AppendFormatted(x);
handler.AppendLiteral(", ");
handler.AppendFormatted(y);
handler.AppendLiteral(")");
string s = handler.ToStringAndClear();

詳細な条件については「C# 10.0 の補間文字列の改善」で別途説明します。

とりあえず、簡単な条件としては、実行環境を .NET 6 以上(TargetFramework を net6.0 以上)にして再コンパイルするだけで文字列補間のパフォーマンスが上がると思ってください。

また、C# 10.0 ではこれと同時に、一定の条件を満たす場合、文字列補間を const にできるようになりました

エスケープ

エスケープ($"" の中で本来使えない文字を埋め込む方法)の方法は通常の文字列とほぼ同じです。 通常の文字列リテラルと同じく、\ に続けることで、"記号(\")や改行文字(\n)などが書けます。

少しだけ違うのは、$"" の中では {} も特別な意味を持っているので、これらに対するエスケープが別途必要になります。{} は2つ重ねて{{}} 書くことで、補間の意味ではなく、その場所に波括弧を表示する意味になります。

var p = new { X = 10, Y = 20 };
Console.WriteLine($"\"{{{p.X}, {p.Y}}}\"");
"{10, 20}"

書式指定

書式指定もできます。

var formatted = $"({12300:c}, {12300:n}, {12300,4:x})";

書式の書き方もstring.Formatに対して使えるものと同じです。

ただ、C#の構文化したことで、元々実行してみるまでエラーがわからなかったのが、コンパイル時に検出できるようになったりしています。

// ほぼ同じ意味
Console.WriteLine(string.Format("{0,4:x}", x));
Console.WriteLine($"{x,4:x}");

// 書き方を忘れて、 , と : を間違えてしまうと…

// 実行時エラー
Console.WriteLine(string.Format("{0,x}", x));

// コンパイル エラー
Console.WriteLine($"{x,x}");

文字列補間と条件演算子

{}の中には割と任意の式を書けます。 たとえば、以下のように、メソッドを呼び出したり、{}の中にさらに文字列リテラル""を含めることもできます。

var data = new[] { 1, 2, 3 };
var s = $"{string.Join(", ", data)} => {string.Join(", ", data.Select(i => i * i))}";

ただ、1つだけ制限があって、条件演算子 ?:は、{}中に直接書くことができません。 たとえば以下のコードでは、1行目(s1の行)がコンパイルエラーになります。

var s1 = $"p = {p == null ? "null" : p.ToString()}"; // エラー
var s2 = $"p = {(p == null ? "null" : p.ToString())}"; // 1段 () でくくればOK

前節の書式指定の : と認識されて、「書式エラー」になります。 (「?がある時だけ:の解釈を変える」というのが高コストすぎるそうで、こういう仕様になっています。) 一応、s2の行のように、1段階 ()でくくればコンパイルできるようになります。

複数行の文字列補間

また、$@ から始めることで、複数行の文字列補間もできます。

var verbatim = $@"
verbatim (here) string
{x}, {y}, {x:c}, {x:n}
";

ちなみに、逆順、つまり、@$は、C# 8.0 以降でだけ使えます(C# 7.3 以前だとコンパイル エラーになります)。

// これは C# 7.3 以前ではコンパイル エラーになる
var verbatim = @$"
verbatim (here) string
{x}, {y}, {x:c}, {x:n}
";

また、$@を使った場合、エスケープのルールは逐語的文字列リテラルと同じになります。 すなわち、" と書きたければ ""と、ダブルクォーテーションを2つ重ねます。また、\から始めるエスケープはできません(\記号がそのまま表示される)。

Console.WriteLine($@"
""
{{
{p.X}\{p.Y}
}}
""
");

"
{
10\20
}
"

FormattableString

ちなみに、Formatメソッドには、IFormatProvider インターフェイス(System名前空間)を与える(カルチャーなどの指定ができる)オーバーロードがあります(参考: 「書式とカルチャー」)。

C# 6 では、文字列補間機能を使いつつ、IFormatProvider を与える方法もちゃんと提供されます。 文字列補間でカルチャー指定するには、これから説明する FormattableString という型(System名前空間)を介します。

文字列補間構文では、以下のように、IFormattable インターフェイス(System名前空間)に代入すると、 一旦 FormattableString クラス(System 名前空間)のインスタンスが作られます。 (左辺の型を見て決定。右辺の書き方は直接文字列に整形する場合とまったく同じ。)

// 左辺の型が IFormattable の時、文字列補間の結果は string ではなく、FormattableString になる
System.IFormattable formatable = $"({x}, {y})";

IFormattableToString メソッドには、IFormatProvider を与えることで、整形の仕方を調整できます。

IFormattable f = $"{x :c}, {x :n}";
Console.WriteLine(f.ToString(null, new System.Globalization.CultureInfo("en-us")));

ちなみに、こちらは、FormattableStringFactory クラス(System.Runtime.CompilerServices 名前空間)の Create メソッド呼び出しに変換されます。

System.IFormattable formatable = System.Runtime.CompilerServices.FormattableStringFactory.Create("({0}, {1})", x, y;

FormattableString のオーバーロード解決

string 引数と FormattableString 引数のオーバーロードがあるとき、 $"" リテラルを渡すと、常に string の方が優先されます。

例えば以下のようなメソッドを考えます。

// string が優先されるので、M1($"") という書き方では呼び分けできない。
static void M1(string s) => Console.WriteLine("string: " + s);
static void M1(FormattableString s) => Console.WriteLine($"format: {s.Format}, args: {string.Join(", ", s.GetArguments())}");

このとき、M1($"") という書き方では M1(string) の方が呼ばれてしまいます。

// string の方が呼ばれる
M1("");
 
// これでも、結局 string の方が呼ばれる
M1($"");
 
// FormattableString の方を呼びたければ明示的なキャストが必要
M1((FormattableString)$"");

FormattableString の方を優先的に呼んでほしい場合は、 以下のようなちょっとしたトリックが必要になります。

// M2("") と M2($"") で呼び分けできる。
static void M2(RawString s) => M1(s.Value);
static void M2(FormattableString s) => M1(s);
 
// オーバーロード解決の優先度をごまかすために、string からの暗黙的型変換を持つ構造体を用意。
public readonly struct RawString
{
    public readonly string Value;
    public RawString(string value) => Value = value;
    public static implicit operator RawString(string s) => new RawString(s);
 
    // これがないとダメみたい
    public static implicit operator RawString(FormattableString s) => throw new InvalidCastException();
}

暗黙的型変換と比べれば FormattableString の方が優先度が高いので、 この M2 であれば、ちゃんと M2("")string の方が、 M2($"")FormattableString の方が呼ばれます。

// RawString (string) の方が呼ばれる
M2("");
 
// これなら FormattableString の方が呼ばれる
M2($"");
 
// ただ、 + とかを加えてしまうと string 扱いになってしまうので注意
M2($"" + $"");

nameof 演算子

C# 6 で、nameof 演算子(nameof operator: "name of X" (Xの名前)を1キーワード化したもの)というものが追加されました。 変数や、クラス、メソッド、プロパティなどの名前(識別子)を文字列リテラルとして取得できます。

using System;

class MyClass
{
    public int MyProperty => myField;
    private int myField = 10;

    public void MyMethod()
    {
        var myLocal = 10;
        Console.WriteLine(nameof(MyClass));
        Console.WriteLine(nameof(MyProperty) + " = " + MyProperty);
        Console.WriteLine(nameof(myField) + " = " + myField);
        Console.WriteLine(nameof(MyMethod));
        Console.WriteLine(nameof(myLocal) + " = " + myLocal);
    }
}
MyClass
MyProperty = 10
myField = 10
MyMethod
myLocal = 10

(ちなみに、nameof 演算子は const にできます。)

こういう識別子名を文字列化したくなる場面の例としてC# で頻出するパターンは、 INotifyPropertyChanged の実装や、ArgumentExceptionの引数などがあります。

例えば、C# 5.0までであれば、ArgumentoExceptionは以下のようにメッセージを書くことになりました。

static double Sqrt(double x)
{
    if (x < 0)
        throw new ArgumentException("x は0以上でなければなりません");
    return Math.Sqrt(x);
}

しかし、この例のように、普通の文字列リテラルとして識別子を書いてしまうと、それが識別子だという情報が失われて、ソースコード解析の対象から外れてしまう問題があります。例えばVisual Studioは、変数、引数、メソッド名など、識別子のリネーム機能を持っていますが、文字列中に埋め込んでしまったものは識別子としては認識されず、リネームできません。

そこで、C# 6で追加されたnameof 演算子を使います。

static double Sqrt(double x)
{
    if (x < 0)
        throw new ArgumentException($"{nameof(x)} は0以上でなければなりません");
    return Math.Sqrt(x);
}

このようなリファクタリング機能を使った際、nameof 演算子であれば、その識別子を使っている個所全ての変更も全て行われます。

(ここから下、文章が古い。図も含めて要修正)

例えば、メソッド名などに一度適当な名前を付けて実装したあと、Visual Studioのリファクタリング機能を使ってちゃんとした名前にリネームしたいことがあります。 しかし、文字列にしてしまっている "" 内のメソッド名の部分はリファクタリングできず、元のまま残ります。

nameof 演算子をリファクタリングの対象にする※
nameof 演算子をリファクタリングの対象にする※

※ 2015/1 版(CTP 5)の Visual Studio 2015 Preview 時点ではメソッド名のリファクタリングは未実装。

※ RC以降はできるように。ただ、オーバーロードが複数あるとすべては拾えない。

nameof 演算子の目的はここにあります。識別子名を文字列化するだけなんですが、ソースコード解析の対象にできます。

INotifyPropertyChanged の実装でもnameof 演算子を使う例を以下に挙げておきましょう。

using System.ComponentModel;
using System.Runtime.CompilerServices;

class Rect : BindableBase
{
    public int Width
    {
        get { return _width; }
        set
        {
            SetProperty(ref _width, value);
            // Width が変化すると Area も変化するので、それを通知
            OnPropertyChanged(nameof(Area));
        }
    }
    private int _width;

    public int Height
    {
        get { return _height; }
        set
        {
            SetProperty(ref _height, value);
            // Height が変化すると Area も変化するので、それを通知
            OnPropertyChanged(nameof(Area));
        }
    }
    private int _height;

    public int Area => Width * Height;
}

public class BindableBase : INotifyPropertyChanged
{
    protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (!Equals(storage, value))
        {
            storage = value;
            OnPropertyChanged(propertyName);
        }
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    public event PropertyChangedEventHandler PropertyChanged;
}

nameof(引数) のスコープ変更

Ver. 11

C# 11 で、nameof にちょっとだけ変更が掛かりました。 以下のように、メソッドに対する属性の中で、そのメソッドの引数の名前が参照できるようになりました。

using System.Diagnostics.CodeAnalysis;

// C# 10 までこの属性、 NotNullIfNotNull("x") と書かないといけなくて割かしつらかった。
[return: NotNullIfNotNull(nameof(x))]
static string? m(string? x) => x;

この例で使っているように、きっかけとしてはnull 許容参照型で使う NotNullIfNotNull 属性などのために仕様変更されました。 これ以降にも、CallerArgumentExpression 属性やInterpolatedStringHandlerArgument属性など、 引数名を参照したい属性がじわじわと増えていたりします。

更新履歴

ブログ