息抜き C# ~ New Normal なコードの書き方:第07回「生文字列リテラル」 ~

こんにちはecbeingでアーキテクトをやっている宮原です。

New Normal なコードの書き方 の第07回目、今日は「生文字列リテラル」の書き方についてご紹介しようと思います。

本記事は 息抜きC# 記事の第07回目です。
第06回目「レコード型」はこちら。

「整形されたHTMLを出力したい」

Webアプリケーション開発という商売柄、「フォーマットされたHTMLを出力したい」という場面が稀によくあります。
出力するHTMLが固定であれば、文字列リテラルを使ってHTML文字列を作成します。
このとき文字列リテラルの書き方と、実際に出力される文字列の間に地味ながら様々な問題が発生します。

今回、下記のHTMLを出力したいという要求があったと仮定し、どの様な問題が発生するのかを説明していきたいと思います。

【出力したいHTML】

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>

通常の文字列リテラル

通常の文字列リテラルとは、C#では最も一般的に利用される文字列リテラルです。

// 通常の文字列リテラル
string name = "ホゲ山ホゲ太郎";

C#を学習する際に、通常の文字列リテラルにはいくつか制約が存在することは学ぶと思いますが、そのうち今回の状況に関連するものを抜粋してみましょう。

通常の文字列リテラルの制約

  1. リテラル内で改行できない
  2. リテラル内に改行コードを埋め込めない
  3. 「\n」で改行コードを表現する必要がある
  4. 「\"」でダブルクォーテーションを表現する必要がある

この制約の中で、「リテラル内で改行できない」という制約が今回のような複数行の文字列を出力場合かなり問題になります。
もちろん、下記のように1行で書けば要件を満たす出力を行うことは可能です。

"<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Blank Page</title>\n</head>\n<body>\n    <script>\n    </script>\n</body>\n</html>"

しかし、このリテラルの可読性はお世辞にも良いとは言えません。
少なくとも自分にはこの1行にまとめられた文字列を一目見て、どんなHTMLなのかを即座に理解することはできません。

なので文字列リテラルを1行にまとめず、下記のように複数に分割し、文字列連結して擬似的に改行を再現させるような書き方をよく採用します。
また、上記の1行HTMLでもやっていますが、実際に出力する文字列を改行させるため各行末に「\n」を追加し、また文字列中の「"」は「\"」に置換しています。

サンプルコード1

string html = ""
    + "<!DOCTYPE html>\n"
    + "<html lang=\"ja\">\n"
    + "<head>\n"
    + "    <meta charset=\"utf-8\">\n"
    + "    <title>Blank Page</title>\n"
    + "</head>\n"
    + "<body>\n"
    + "    <script>\n"
    + "    </script>\n"
    + "</body>\n"
    + "</html>";

Console.WriteLine("-------------------------");
Console.WriteLine(html);
Console.WriteLine("-------------------------");

サンプルコード1の出力結果

-------------------------
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>
-------------------------

通常の文字列リテラルを利用した場合のイマイチな部分

出力結果自体には問題はありませんが、コードにいくつか不満が残ります。

  1. リテラル内で改行できないため、1行ごとにリテラルを作る必要がある
  2. 全行で文字列連結演算子が必要
  3. 全行に開始・終了引用符が必要
  4. 改行コードの代わりに「\n」を使う必要がある
  5. 「"」の代わりに「\"」を使う必要がある

もっと良い文字列リテラルの書き方は無いものでしょうか?

逐語的文字列リテラル

より良い書き方を求めて、逐語的文字列リテラルを試してみましょう。

// 逐語的文字列リテラル
string path = @"C:\Windows\System32\drivers\etc";

パスを書くときに使うことでおなじみの逐語的文字列リテラルですが、
実は「\」をエスケープしなくてもいいという特徴以外に、リテラル内で改行できるという利点を持っています。
まずは逐語的文字列リテラルを使ったサンプルコードを見ていきましょう。

サンプルコード2(逐語的文字列リテラル)

string html = @"
<!DOCTYPE html>
<html lang=""ja"">
<head>
    <meta charset=""utf-8"">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>
";

Console.WriteLine("-------------------------");
Console.WriteLine(html);
Console.WriteLine("-------------------------");

見やすいコードですね。かなり良い。
「"」を「""」に置き換える必要はありますが、文字列リテラルを見れば出力されるHTMLが容易に想像できます。

サンプルコード2の出力結果

-------------------------

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>

-------------------------

出力結果も概ね問題ありません。
ただ、HTMLの先頭と末尾に不要な改行が入っているのが気になると言えば気になります。
これは逐語的文字列リテラルが開始行と終了行の改行も、リテラルの一部として含んでしまうために起こる現象です。

【不要な改行が入る理由】
string html = @"  ←ここの改行コードと
・
・
・
</html>  ←ここの改行コードが文字列に含まれてしまう
";


これを解消するコードに修正してみましょう。

サンプルコード3(逐語的文字列リテラルの先頭と末尾の改行を削除)

string html = @"<!DOCTYPE html>
<html lang=""ja"">
<head>
    <meta charset=""utf-8"">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>";

Console.WriteLine("-------------------------");
Console.WriteLine(html);
Console.WriteLine("-------------------------");

上記のように修正することで、下記のような出力結果を得ることができます。

サンプルコード3の出力結果

-------------------------
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>
-------------------------

出力結果としては完璧です。
しかし、コードを修正したことで以下のような不満点が出ました。

【コード修正による不満点】
string html = @"<!DOCTYPE html> ←ここが
<html lang=""ja"">        ←この部分とインデントが揃わない
・
・
・
</html>";  ←最終行に「";」が必要になる

とはいえこの程度の不満であれば許容可能とする人は多いと思います。
自分も不満点がこれだけであれば逐語的文字列リテラルを利用したでしょう。

しかし、逐語的文字列リテラルには致命的欠点が存在します。

サンプルコード4(逐語的文字列リテラルでトップレベルステートメントを使わない書き方)

実のことを言うとこの致命的な問題を隠すため、あえて今までトップレベルステートメントでサンプルコードを書いてきました。
しかし当然ながら常にトップレベルステートメントでコードを書けるわけではありません。

namespace 逐語的文字列リテラル3
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string html = @"<!DOCTYPE html>
            <html lang=""ja"">
            <head>
                <meta charset=""utf-8"">
                <title>Blank Page</title>
            </head>
            <body>
                <script>
                </script>
            </body>
            </html>";

            Console.WriteLine("-------------------------");
            Console.WriteLine(html);
            Console.WriteLine("-------------------------");
        }
    }
}

トップレベルステートメントを使わない書き方だとこうなります。
プログラムコード上に問題はありません。

サンプルコード4の出力結果

-------------------------
<!DOCTYPE html>
            <html lang="ja">
            <head>
                <meta charset="utf-8">
                <title>Blank Page</title>
            </head>
            <body>
                <script>
                </script>
            </body>
            </html>
-------------------------

プログラムコード上に問題はありませんでしたが、出力結果に問題が発生してしまいました。
出力されたHTMLのインデントがおかしくなっています。

実は逐語的文字列リテラルは改行コードをそのまま文字列にするだけでなく、インデントもそのまま文字列にしてしまいます。

【インデントが崩れた理由】
・
・
・
        static void Main(string[] args)
        {
            string html = @"<!DOCTYPE html>
            <html lang=""ja"">
↑           ↑
この部分のインデントがそのまま文字列になってしまう…
・
・
・

言われてみればそうか、という気もしなくは無いですが、「そうか」で済ませるほどこの問題は小さくありません。

出力文字列のインデントが狂わないように逐語的文字列リテラルで表現しようとすると、以下のようなコードになってしまいます。

サンプルコード5(逐語的文字列リテラル内にインデントを入れない書き方)

namespace 逐語的文字列リテラル4
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string html = @"<!DOCTYPE html>
<html lang=""ja"">
<head>
    <meta charset=""utf-8"">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>";

            Console.WriteLine("-------------------------");
            Console.WriteLine(html);
            Console.WriteLine("-------------------------");
        }
    }
}

出力結果は省略しますが、この書き方であれば出力文字列のインデントは崩れず期待通りの出力結果が得られます。
しかし見ての通りコードの方のインデントが完全に崩れてしまいました。
つまり出力結果のインデントを正しくしようとするとコードのインデントが崩れ、
コードのインデントを正しくしようとすると出力結果のインデントが崩れる、
というジレンマが発生するのです。
(もちろん、if文やfor文などでネストが深くなればインデントの不整合は更に深刻になります。)

逐語的文字列リテラルによるHTML出力には致命的な問題が立ちはだかるのです。

生文字列リテラル

とはいえ逐語的文字列リテラルは非常に惜しいところまで行っていました。
しかしあと一歩のところで採用することができません(※個人の感想です)。

この問題は長年C#プログラマーを悩ませてきたものの、あまりメジャーでも深刻でもない問題なので、ずっと放置されてきました(※個人の感想です)。

ところが2022 年11 月にリリースされたC#11でついに待望の新機能(※個人の感想です)が追加されることになりました。

その新機能こそが「生文字列リテラル」です。

// 生文字列リテラル
string raw = """
    "も\も使いたい放題
    改行もできるよ!!
    """;

まずは生文字列リテラルを使用したサンプルコードとその出力結果を御覧いただきましょう。

サンプルコード6(生文字列リテラル)

namespace 生文字列リテラル
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string html = """
                <!DOCTYPE html>
                <html lang="ja">
                <head>
                    <meta charset="utf-8">
                    <title>Blank Page</title>
                </head>
                <body>
                    <script>
                    </script>
                </body>
                </html>
                """;

            Console.WriteLine("-------------------------");
            Console.WriteLine(html);
            Console.WriteLine("-------------------------");
        }
    }
}

サンプルコード6の出力結果

-------------------------
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Blank Page</title>
</head>
<body>
    <script>
    </script>
</body>
</html>
-------------------------

いかがでしょうか?
完璧ですね。
リテラルの表現と出力内容が完全に一致しています。
また、単純に一致しているだけでなく文字リテラルはネストに合わせた適切なインデントが行われています。

自分がこの生文字列リテラルを見たときは、思わず

こういうのが欲しかったんだよ!!

と心のなかでガッツポーズを取ったほどです。

生文字列リテラルのココが凄い!!*1

なぜ生文字列リテラルはこのようなことを実現させることができたのでしょうか?
リテラル表現と文字列出力の一致させるため、生文字列リテラルが備えた数々の機能を称賛を交えながら紹介していきましょう。

  • 改行コードをそのまま使えて凄い!*2
  • 「"」が文字列内でそのまま使えて凄い!!*3
  • 最初と最後の改行コードを自動的に削除してくれて凄い!!!*4
  • 文字列リテラル内のインデント空白を自動的に削除してくれて凄い!!!!*5
  • 他にも凄い機能があって凄い!!!!!*6

まとめ

今回テーマとした「HTML出力」は分野として「コード生成」に当たります。
近年、生成AIや低コード/ノーコードプログラミングの流行により、コード生成プログラミングは活発になってきていて、C#が新しく生文字列リテラルを導入したのもこの流れを見据えてのことだと思われます。
生文字列リテラルは HTMLの生成以外にも JSON や SQL、その他あらゆるコードの生成に利用できますので、C#11を使える環境の方々はぜひ生文字列リテラルを活用してみて下さい!!

第08回目はこちら。(近日公開予定)

ecbeingではコード生成に興味があるエンジニアを募集しています!!

careers.ecbeing.tech

*1:今回は複数行形式の生文字列リテラルのみ紹介しましたが、単一行形式の生文字列リテラルもあります。気になった方は調べてみて下さい

*2:逐語的文字列リテラルでも可能だったことですが(^^)

*3:デフォルトの引用符が「"""」なので「"」を特別扱いする必要が無くなったからです

*4:先頭・末尾の改行は無視されます

*5:終了引用符のインデント位置を基準として、文字列内のインデントに利用された空白を削除してくれます

*6:今回の記事では触れませんが、その他「引用符が変更可能」「文字列補間が可能」「文字列補間記号が変更可能」などの凄い機能があります