概要
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 = 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})";
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($"") という書き方では呼び分けできない。
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のリファクタリング機能を使ってちゃんとした名前にリネームしたいことがあります。 しかし、文字列にしてしまっている "" 内のメソッド名の部分はリファクタリングできず、元のまま残ります。
※ 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
属性など、
引数名を参照したい属性がじわじわと増えていたりします。