目次

キーワード

概要

Ver. 6

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

文字列挿入

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

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

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

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

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

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

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

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

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

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

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

エスケープ

エスケープ($"" の中で本来使えない文字を埋め込む方法)の方法もstring.Formatと同じです。

通常の文字列リテラルと同じく、\ に続けることで、"記号(\")や改行文字(\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}
";

ちなみに、逆順、つまり、@$ではダメです(コンパイル エラーを起こします)。

// これはコンパイル エラーになる
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 という型を介します。

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

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

IFormattable の ToString には、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;

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

こういう識別子名を文字列化したくなる場面の例として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;
}

更新履歴

ブログ