概要
Ver. 6
C# 6 で、補間文字列と、nameof 演算子(nameof operator)という、2つの文字列関連機能が追加されました。
また、C# 11 で、生文字列リテラルという構文が追加されました。
文字列補間
クラスのメンバーを整形して文字列化するには、.NETではstringのFormatメソッドを使います。
var formatted =string.Format("({0}, {1})", x, y);しかし、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 =newDefaultInterpolatedStringHandler(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})";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;FormattableString のオーバーロード解決
string 引数とFormattableString 引数のオーバーロードがあるとき、$"" リテラルを渡すと、常にstring の方が優先されます。
例えば以下のようなメソッドを考えます。
// string が優先されるので、M1($"") という書き方では呼び分けできない。staticvoidM1(strings) =>Console.WriteLine("string: " +s);staticvoidM1(FormattableStrings) =>Console.WriteLine($"format:{s.Format}, args:{string.Join(", ",s.GetArguments())}");このとき、M1($"") という書き方ではM1(string) の方が呼ばれてしまいます。
// string の方が呼ばれるM1("");// これでも、結局 string の方が呼ばれるM1($"");// FormattableString の方を呼びたければ明示的なキャストが必要M1((FormattableString)$"");FormattableString の方を優先的に呼んでほしい場合は、以下のようなちょっとしたトリックが必要になります。
// M2("") と M2($"") で呼び分けできる。staticvoidM2(RawStrings) =>M1(s.Value);staticvoidM2(FormattableStrings) =>M1(s);// オーバーロード解決の優先度をごまかすために、string からの暗黙的型変換を持つ構造体を用意。publicreadonlystructRawString{publicreadonlystring Value;publicRawString(stringvalue) => Value =value;publicstaticimplicitoperatorRawString(strings) =>newRawString(s);// これがないとダメみたいpublicstaticimplicitoperatorRawString(FormattableStrings) =>thrownewInvalidCastException();}暗黙的型変換と比べれば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;classMyClass{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); }}MyClassMyProperty = 10myField = 10MyMethodmyLocal = 10(ちなみに、nameof 演算子は const にできます。)
こういう識別子名を文字列化したくなる場面の例としてC# で頻出するパターンは、INotifyPropertyChanged の実装や、ArgumentExceptionの引数などがあります。
例えば、C# 5.0までであれば、ArgumentoExceptionは以下のようにメッセージを書くことになりました。
staticdouble Sqrt(double x){if (x < 0)thrownewArgumentException("x は0以上でなければなりません");returnMath.Sqrt(x);} しかし、この例のように、普通の文字列リテラルとして識別子を書いてしまうと、それが識別子だという情報が失われて、ソースコード解析の対象から外れてしまう問題があります。例えばVisual Studioは、変数、引数、メソッド名など、識別子のリネーム機能を持っていますが、文字列中に埋め込んでしまったものは識別子としては認識されず、リネームできません。
そこで、C# 6で追加されたnameof 演算子を使います。
staticdouble Sqrt(double x){if (x < 0)thrownewArgumentException($"{nameof(x)} は0以上でなければなりません");returnMath.Sqrt(x);} このようなリファクタリング機能を使った際、nameof 演算子であれば、その識別子を使っている個所全ての変更も全て行われます。
(ここから下、文章が古い。図も含めて要修正)
例えば、メソッド名などに一度適当な名前を付けて実装したあと、Visual Studioのリファクタリング機能を使ってちゃんとした名前にリネームしたいことがあります。しかし、文字列にしてしまっている "" 内のメソッド名の部分はリファクタリングできず、元のまま残ります。
nameof 演算子の目的はここにあります。識別子名を文字列化するだけなんですが、ソースコード解析の対象にできます。
INotifyPropertyChanged の実装でもnameof 演算子を使う例を以下に挙げておきましょう。
using System.ComponentModel;using System.Runtime.CompilerServices;classRect :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 classBindableBase :INotifyPropertyChanged{protected void SetProperty<T>(refT 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,newPropertyChangedEventArgs(propertyName));public eventPropertyChangedEventHandler PropertyChanged;}。
nameof(引数) のスコープ変更
Ver. 11
C# 11 で、nameof にちょっとだけ変更が掛かりました。以下のように、メソッドに対する属性の中で、そのメソッドの引数の名前が参照できるようになりました。
using System.Diagnostics.CodeAnalysis;// C# 10 までこの属性、 NotNullIfNotNull("x") と書かないといけなくて割かしつらかった。[return:NotNullIfNotNull(nameof(x))]staticstring?m(string?x)=>x;この例で使っているように、きっかけとしてはnull 許容参照型で使うNotNullIfNotNull 属性などのために仕様変更されました。これ以降にも、CallerArgumentExpression 属性やInterpolatedStringHandlerArgument属性など、引数名を参照したい属性がじわじわと増えていたりします。
unbound な型に対する nameof
Ver. 14
C# 14 から、T<> みたいに型引数を埋めていないジェネリック型(これを unbound (未束縛)とか open (開きっぱなし) な型といいます)に対してnameof 演算子を使えるようになりました。
Console.WriteLine(nameof(List<>));// "List"Console.WriteLine(nameof(Dictionary<,>.Keys));// "Keys"Console.WriteLine(nameof(List<>.Enumerator.MoveNext));// "MoveNext"
nameof 演算子では元からどのみち型が引数の部分 (<> とその中身)は無視されていたので、ここを埋めるかどうかは結果得られる文字列に何の影響もありません。これまでできなかったのは「手間に対して需要が少ない」という実装上の都合で、C# 14 でようやく着手という流れです。(typeof(T<>) は昔から書けたのでそれの流用でできそうに見えますが、typeof の場合はtypeof(T<>.Member) みたいなメンバー参照がないので、今回のnameof 対応はそれなりに新規実装の部分があります。)
C# 13 以前だと同じことをしたければ、意味もなく何か適当な型引数を埋めて書いていました。
// int の部分には特に意味はないけども、埋めないとコンパイルが通らなかったので適当に int を採用。Console.WriteLine(nameof(List<int>));// "List"Console.WriteLine(nameof(Dictionary<int,int>.Keys));// "Keys"Console.WriteLine(nameof(List<int>.Enumerator.MoveNext));// "MoveNext"
ただ、型引数にかかっている制約によっては「適当にint を渡す」みたいなことがかなり難しくなります。場合によっては、以下のように「絶対に書けない」という状況も発生します。(この場合、メソッドM が public なのがおかしいというのはありますが、原理的にはこういうことがありえます。)
publicinterfaceI{// static abstract があると M<I> と書けなくなる。// (実装したクラスでないと渡せない。)publicstaticabstractvoidM();}publicabstractclassB{// アクセス制限がかなり厳しいコンストラクターを用意。// クラス自体は public であっても、別プロジェクトで派生クラスは作れない。privateprotectedB() { }}// 実装しているクラスは internal で、外からは使わせない。internalclassD :B,I{publicstaticvoidM() { }}publicstaticclassC{// T : I のせいで派生クラスでないとダメ。// T : B のせいで派生クラスを作れない。// 唯一の実装クラス D は internal なので、外からは使えない。// 結果、C# 13 以前は nameof(M<>) が使えなかった。publicstaticvoidM<T>()whereT :B,I { }}


