Enhancing String Literals Delimiters to Support Raw Text
01 何が問題だったのか
Swiftの文字列リテラルでは、バックスラッシュ(\)がエスケープ文字として働きます。\n・\t・\"・\\ といった特殊文字の表現、\u{n} によるUnicodeスカラーの記述、\(...) による文字列補間などはすべてこのエスケープに依存しています。
便利な仕組みですが、バックスラッシュや二重引用符を多く含む文字列を書くときには、エスケープが読みづらさの原因になります。特に問題になるのが次のようなケースです。
正規表現
バックスラッシュを多用する正規表現をそのまま文字列リテラルに書こうとすると、\\ の形にエスケープし直さなければなりません。
// そのまま書きたいパターン:
// enum\s+.+\{.*case\s+[:upper:]
let ucCaseCheck = "enum\\s+.+\\{.*case\\s+[:upper:]"
元のパターンと文字列リテラル上の表現が一致しないため、コピーして試す・戻すといった往復が面倒になりますし、エスケープ漏れのバグも起きやすくなります。
既にエスケープされたデータ
JSONやJavaScript、正規表現など、既にエスケープを含む文字列をソースに埋め込む場合、Swiftのエスケープと二重にかかってしまい、バックスラッシュを倍々に書き足す必要があります。テスト用にコピー&ペーストしたデータをそのままコードに貼り付けられない、というだけでも十分な負担です。
引用符をたくさん含むテキスト
セリフなどを含むテキストでは、二重引用符をすべて \" に書き換える必要があります。
// 書きたいテキスト:
// Alice: "How long is forever?" White Rabbit: "Sometimes, just one second."
let quote = "Alice: \"How long is forever?\" White Rabbit: \"Sometimes, just one second.\""
Windowsパスやコード生成
C:\Windows\All Users\Application Data のようなWindowsパスや、他言語のコードを文字列として生成するメタプログラミングでも、バックスラッシュの二重化が常に付きまとっていました。
これらに共通するのは、「ソース中の見た目」と「実際の文字列の内容」を一致させたい、という要求です。Swiftには、エスケープを解釈せずにテキストをそのまま保持できる、いわゆる raw string リテラル が存在しませんでした。一方、他の言語(Python、Rust、C++、C# など)には何らかの形でraw stringが用意されており、Swiftでも同様の機能が望まれていました。
課題は、どういう構文を選ぶかです。r"..." のような先頭プレフィックス、@"..."、バッククォートなどはいずれもSwiftの既存構文や他言語の慣習と衝突するか、「Swiftらしくない」見た目になるという理由で退けられてきました。
02 どのように解決されるのか
新しい「raw string型」を別に追加するのではなく、既存の文字列リテラル構文を拡張 して、カスタマイズ可能な区切り子(delimiter)を持てるようにします。Rustの設計にインスパイアされつつ、先頭の r のような目印は付けず、従来の文字列と見た目が連続するように整理されたのが特徴です。
# でリテラルを囲む
文字列リテラルの前後にある " の外側に、同じ個数の #(Number Sign, U+0023)を足すことができます。
"これは従来どおりの文字列"
#"これも文字列リテラル"#
####"これも文字列リテラル"####
"、#"..."#、##"..."## はどれも同じ文字列値を表します。大事なのは、開始側と終了側で # の個数を一致させることと、そのリテラル内では 区切り子自体が " ではなく "#(または "## など)になる ことです。区切り子の一部に使われないただの " は、文字列の中身として扱われます。
#"She said, "This is dialog!""#
// 通常のリテラルで書くと "She said, \"This is dialog!\"" と同じ
中身に "# そのものを含めたいときは、外側の # の数を増やして調整します。
##"このリテラルは "# を含めてもそのまま書ける"##
エスケープ区切り子も # で調整される
この設計の中心は、エスケープを始めるための記号(エスケープ区切り子)もリテラルの # の個数に合わせて変わる、という点です。
| リテラルの区切り子 | エスケープ区切り子 |
|---|---|
"..." |
\ |
#"..."# |
\# |
##"..."## |
\## |
######"..."###### |
\###### |
区切り子に # が1つ付いたリテラルの中では、エスケープを開始するには \# と書かなければなりません。ただの \ はエスケープと解釈されず、バックスラッシュそのもの として扱われます。
#"これは補間されません: \(foo)"# // \ と ( と foo と ) がそのまま入る
#"これは補間されます: \#(foo)"# // \#(...) が補間として働く
#"改行ではなくバックスラッシュ+n: \n"# // \n と書いても改行にはならない
#"これは改行になります: \#n"# // \#n は改行
つまり、# の数を増やしたリテラルはデフォルトでraw文字列として振る舞い、必要なときだけ \#... を書けば補間や特殊文字のエスケープを使える、という設計です。
複数行リテラルでも同じ
"""...""" の複数行リテラルにも、同じ要領で # を付けられます。
#"""
raw文字列で \r\n をそのまま書ける
改行したければ \#n と書けば補間される
"""#
# で囲んだ複数行リテラル中にも、通常の複数行リテラルと同じインデント剥がしのルールがそのまま適用されます。
具体例
正規表現やWindowsパスは、エスケープから解放されて元の表記のまま書けるようになります。
let ucCaseCheck = #"enum\s+.+\{.*case\s+[:upper:]"#
let path = #"c:\windows\system32"#
let phone = #"\d{3} \d{3} \d{4}"#
引用符を含むセリフもそのまま書けます。
let quote = #"Alice: "How long is forever?" White Rabbit: "Sometimes, just one second.""#
既にエスケープされたJSONを壊さずに埋め込みつつ、一部だけ補間することもできます。
let message = #"""
[
{
"id": "\#(idNumber)",
"title": "A title that \"contains\" \\\""
}
]
"""#
従来ならSwiftが \" や \\ を先に解釈してしまい、JSONとしては壊れた文字列になっていた内容が、そのままの表現で保持されます。
不正なエスケープはエラーになる
# 付きのリテラルの中で、エスケープ区切り子に余分な # が付いてしまった場合はコンパイルエラーになります。
#"printf("%s\n", value_string)"# // OK、\n は中身として扱われるだけ
#"printf("%s\#n", value_string)"# // OK、\#n は改行
#"printf("%s\#x\#n", value_string)"# // エラー: \#x は不正なエスケープ
コンパイラは、必要に応じて「リテラルの両端の # を増やす」「エスケープの # を減らす」といった修正案を提示します。
補間との相性
通常のリテラルでは従来どおり \(expr) で補間できますし、#"..."# の内側では \#(expr) で補間できます。# の数だけ揃えれば、raw文字列の中でも補間を使った動的な組み立てが可能です。特に、コード生成のようにエスケープを温存したいのに一部だけ値を差し込みたい、という用途で有効に働きます。
使いどころ
この機能は、次のような場面で特に効果を発揮します。
- 正規表現のパターン(ネイティブな正規表現構文が入った後も、文字列として渡すAPIではしばらく使われ続けます)
- JSON / XML などのデータフォーマットや、埋め込みDSLのスニペット
- 他言語のコードを生成するメタプログラミング
- Windowsパスのようなバックスラッシュを多く含むパス
- チュートリアルや教材で、Swift以外の言語のコード片を掲載するケース
逆に、"C:\\AUTOEXEC.BAT" のようなエスケープが軽微なケースでは、無理に # を使う必要はありません。raw化するかどうかは読みやすさの観点で選べばよく、従来の文字列リテラルの挙動には何の変更もない、というのがこの設計のポイントです。