生文字列リテラル

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"
}

更新履歴

ブログ