生文字列リテラル
C# 11 で、3つ以上の連続した "
を使うことで、「一切エスケープが必要ない文字列リテラル」を書けるようになりました。
// """ から始まる文字列リテラル(raw string, 生文字列)。
var quote = """
" はそのまま " として使われて、
\ も \ のままの意味。
\\ は \ が2個。
{} とかも特別な解釈はされない。
""";
これを生文字列リテラル(raw string literal)と言います。
最近は「言語内言語」みたいなものの需要が微妙に高まっている中、
こういう「エスケープ不要の文字列」への要望が強くなってきています。
本来ならば逐語的文字列リテラル(@""
)がその役割に当たるんですが、この @""
の構文が微妙に使いにくいので、それを置き換えるような新しい文法が導入されました。
背景: 通常の文字列リテラルや、逐語的リテラル
多くのプログラミング言語で、通常、"
や '
などの記号で挟まられた部分が文字列リテラルになります。
この「通常の文字列リテラル」で困るのは、その文字列中に "
や '
自身を含む場合で、
C# ではそういう場合のために、\
を使ったエスケープを行います。
// " を含む文字列リテラル。
var quote = "\"";
エスケープが必要な文字が増えてくるとかなり煩雑です。
そこで C# では、@""
という書き方で、以下のように、エスケープを減らせるようにしました。
これを逐語的文字列リテラル(verbatim string literal)と言います。
\
は\
としてそのまま使われる- リテラル中に改行を含められる
// @"" と書くと、\ と改行のエスケープが不要に。
var quote = @"これで3行の文字列になる。
\ は \ のまま使われる。\\ も \ 2つ。
ただし、"" を使いたいときは "" を2個並べないとダメ。これでダブルクォーテーションマーク1つ扱い。";
「エスケープなしで書ける文字列」というのが逐語的文字列の存在意義なんですが、
もうこの時点で、「"
にはエスケープが必要」となっています。
その他、文字列補間との組み合わせでは {}
のエスケープも必要です。
また、もう1つの要望として、「複数行の文字列を書くとき、インデントを揃えたいけどできない」という問題もあります。
var value = 123;
// $@"" で逐語的 + 文字列補間。
// - { を使いたければ {{ というように、そこそこ使いたくなりがちな文字に結局エスケープが必要
// - 最初と最後の行の改行も文字列に含まれる
// - インデントのスペース4つも文字列に含まれる
var quote = $@"
{{
""key"": {value}
}}
";
新文法: 生文字列
"
や '
を含め、あらゆる文字を一切エスケープなしで書けるようにしたいということで、
C# 11 で、"""
というように、「3つ以上の "
を並べる」という新しい文法を追加しました。
以下のように、単一行か複数行かと、文字列補間の有無によって4パターンあります。
var value = 123;
var singleLine = """{ "abc": 123 }""";
var mutiLine = """
{
"abc": 123
}
""";
var singleLineInterpolation = $"""abc: {value}""";
var mutiLineInterpolation = $"""
abc: {value}
""";
3つ以上の "
生文字列の目的は「一切のエスケープが不要」というものです。
そこで通常問題になるのが、"""
の内側で同じく """
を使いたい場合。
例えばの話、「自分自身を文字列リテラル化したい」みたいなことを考えてみましょう。 まず、以下のような C# 11 コードがあったとします。
var mutiLine = """
{
"abc": 123
}
""";
一切エスケープ不要というなら、「この C# コードを出力する C# コード」みたいなものもエスケープなしで書けるようにしたいです。
こういう場合に、以下のようなコードを書いてしまうと、最初の """
が出て来た時点で文字列リテラルを閉じようとしてしまって、コンパイル エラーになります。
// """ と """ の間に """ は書けない。
Console.WriteLine("""
var mutiLine = """
{
"abc": 123
}
""";
""");
そこでどうするかというと、生文字列リテラルの開始文字を """"
と4つに増やします。
(同じ個数の "
が出てくるまで文字列リテラルが終わりません。)
// " 4つで開始すれば、リテラルの中で """ (" 3つ)を書いても問題ない。
Console.WriteLine(""""
var mutiLine = """
{
"abc": 123
}
""";
"""");
これが、C# の生文字列リテラルの仕様が「3つ以上の "
を並べる」になっている理由です。
もちろんさらに入れ子を増やして、"""""
(5つ)の内側に """"
を書くこともできます。
Console.WriteLine("""""
Console.WriteLine(""""
var mutiLine = """
{
"abc": 123
}
""";
"""");
""""");
逆に "
2つがダメなのは、""
が既存の文法で有効なもの(空文字列になる)なので、
意味を変えるわけにはいかないからです。
// 生文字列の "+" ではなく、空文字列2つの結合(= 結局は空文字列)。
Console.WriteLine(""
+
"");
単一行と複数行
単一行リテラルか複数行リテラルかは、単純に """
の後ろに改行があるかどうかで変わります。
// 単一行生文字列。
var singleLine = """この中身が文字列リテラル""";
// 複数行生文字列。
var multiLine = """
この行が文字列リテラル。この前後には改行文字は残らない。
""";
// 以下の3行は全く同じ結果になる。
Console.WriteLine("a\"b");
Console.WriteLine("""a"b""");
Console.WriteLine("""
a"b
""");
// 以下の3行も全く同じ結果。
// (C# ソースコードの改行コード次第。この例の場合は LF。
Console.WriteLine("abc\ndef");
Console.WriteLine(@"abc
def");
Console.WriteLine("""
abc
def
""");
ちょっと変わっているのは、複数行リテラルの場合、"""
と改行の間にスペースが挟まっていても複数行生文字列リテラルと認識されます。
// """ の後ろに実はスペースが4つあるけど、それは無視される。
// (ファイルの改行コード次第で 7 か 8。
// abcdef の6文字 + \r\n (改行)。
Console.WriteLine("""
abc
def
""".Length);
今のところは開き """
の後ろに書いても OK (ただし無視される)なのは空白文字だけですが、
生文字列の仕様のインスパイア元が Markdown の ```
なので、
もしかしたら以下のような「文字列の中身が何かの注釈を付ける」みたいな仕様は将来認められる可能性はあります。
// C# 11 としては不正。
// 「将来もしかしたら」程度の構文案。
Console.WriteLine("""json
{
"id": 123,
"name": "abc"
}
""".Length);
また、複数行生文字列では、以下のように、「1行たりとも中身がないリテラル」は書けません。
// 先頭・末尾の改行は無視されるので、これが空文字列。
Console.WriteLine("""
""");
// じゃあ、これは?…
// 「空文字列よりも短い文字列リテラル」というのも変で、単にコンパイル エラーに。
Console.WriteLine("""
""");
複数行生文字列とインデント
元々インデントが深い場所で逐語的文字列リテラルを書いた場合、 以下のように、普段の C# コードと同じようなインデントを付けれないという問題があります。
class A
{
public static void M(bool flag, int count)
{
if (flag)
{
for (int i = 0; i < count; i++)
{
Console.WriteLine(@"
インデントが崩れる。
左寄せにしないとリテラルにスペースが含まれちゃう。
");
}
}
}
}
一方、生文字列では自由にインデントを入れられます。
以下のように、閉じ """
の行のインデントを基準にして、それよりも左側のスペースはコンパイル結果には残りません。
class A
{
public static void M(bool flag, int count)
{
if (flag)
{
for (int i = 0; i < count; i++)
{
Console.WriteLine("""
インデントして大丈夫。
ここよりも左側のスペースはコンパイル結果の文字列には含まれない。
"""); // この行のインデントが基準で、そこから前のスペースが消える。
}
}
}
}
ただ、これはこれで逆に、以下のようなコードには注意が必要です。
// 1
Console.WriteLine("""
a
""".Length);
// 5
Console.WriteLine("""
a
""".Length); // 犯人はこの行。インデントがずれてる。
ちなみに、以下のように、閉じ """
の行よりもインデントが少ないコードを書くとコンパイル エラーになります。
// インデントが不正(足りない)なのでエラーに。
Console.WriteLine("""
a
""");
空白文字の混在
C# は通常の(ASCII 文字の)スペース(文字コード U+0020)以外にも、以下のような文字を空白文字とみなします(通常スペースと同じ扱いになります)。
- Unicode の文字カテゴリーが Zs (Space Separator)の文字
- 水平タブ(U+0009)
- 垂直タブ(U+000B)
- フォーム フィード(U+000C)
これらの空白文字を閉じ """
の行に使った場合、途中の行にも全く同じ順序で同じ文字を並べなければなりません。
見えない文字なので少しわかりにくいですが、以下のコードでは1つ目の生文字列はOKで、2つ目(意図的に違う文字を混ぜたもの)はコンパイル エラーになります。
Console.WriteLine(""" この行は OK """); // U+1680 Ogam Space (見える空白文字。古アイルランドで使ってたらしい) Console.WriteLine(""" 違う空白文字を混ぜてしまうとコンパイル エラー。 """);
(幾分かわかりやすくするために、「見える空白文字」である Ogam Space という文字を使っています。 ちなみに、エラーになっている行はこの Ogam Space と通常スペースの混在です。)
注意: @"" 優先
1つ非常に紛らわしい書き方がありまして… 以下のコード、出力はどうなるでしょう?
Console.WriteLine(@"""abc""");
答えは "abc"
です。両端に "
が付いてきます。
これ、@"
から始まっているので逐語的文字列リテラルの方になります。
で、@""
の中では「"
を書きたければ ""
と書く」というエスケープをしますので、
「@"""abc"""
は "abc"
として解釈される逐語的文字列リテラル」ということになります。
@
は見落としがちな文字なので多少注意が必要です。
生文字列、かつ、文字列補間
「生文字列で文字列補間をしたい」という要望もそれなりにあります。 例えば以下のような感じのコードは、そのものはないにしても似たようなコードは書きたいことがあると思います。
Console.WriteLine(format(123, "abc"));
static string format(int id, string name) => $"""
id: {id}
name: "{name}"
""";
補間をやるなら「{
を含めたいときにエスケープが必要になってしまう」という懸念があって、
当初は前向きに検討されていませんでした。
ただ、最終的に、「"
と同じく $
の個数も可変にして解決」という手段を採りました。
「$
の個数と同じ数の {
と }
を書いたときだけ補間あつかい、それ以下の場合は普通の文字列として {
と }
を解釈」となります。
例えば、「文字列補間で JSON を作る」みたいなことをしたい場合、{
を多用することになるわけですが、
この場合は $
を2個にすることで、{
と }
1個はただの文字になって、{{}}
が文字列補間になります。
Console.WriteLine(format(123, "abc"));
static string format(int id, string name) => $$"""
{
"id": {{id /* ここは補間 */ }},
"name": "{{name /* ここも補間 */}}"
}
""";
{
"id": 123,
"name": "abc"
}