この記事の要点
- Swift 5 では SE-0200 により、文字列リテラルの区切りを
#で拡張できるようになりました。#"..."#のように両端を#で囲むと、内部のエスケープシーケンス(\nなど)が文字どおりに解釈されます。 - ほかの言語の「raw string(生文字列)」と違い、Swift の設計ではエスケープも一緒にカスタマイズします。
#の数に合わせて\#のようにエスケープトークンも変わるため、補間(interpolation)を含むすべてのエスケープを残したまま使えます。完全な「raw」ではないため、記事では「medium rare(半生)」な文字列とも呼んでいます。 - 区切りの
#の数は両端で一致させます。#を使うときは、エスケープも#""#なら\#、##""##なら\##のように同じ数の#を付けます。#の数は「必要最小限」が原則で、0 か 1 でほぼ事足ります。 - 正規表現、JSON、コード生成、バックスラッシュの多い文字列などを、エスケープし直す手間なくそのまま貼り付けられるのが主な利点です。
背景: エスケープシーケンスと raw string
エスケープシーケンスは、\\、\"、\u{n} のようにバックスラッシュを前置して、通常の文字列リテラル内では表現しづらい文字を表す仕組みです。Swift では \0(ヌル文字)、\\(バックスラッシュ)、\t(水平タブ)、\n(改行)、\r(復帰)、\"、\' といった特殊文字、\u{n} 形式の Unicode スカラ、そして \( と ) で囲む補間式が使えます。たとえば "hello\n\n\tworld" は、1 行目に “hello”、3 行目にタブで字下げされた “world” を持つ 3 行の文字列になります。
一方で raw string は、エスケープシーケンスを解釈せず内容をすべて文字どおりに扱います。raw string では \n は「改行」ではなく「バックスラッシュと文字 n」を表します。これは正規表現、コードを生成・表示するアプリ、アプリ内ソースコード、JSON や XML のようにあらかじめエスケープされたコンテンツを扱う場面で役立ちます。
raw string は C#、Perl、Rust、Python、Ruby、Scala など多くの言語にあり、区切り方は言語ごとに異なります。多くは q、R、r などの接頭辞で raw を示しますが、Rust と Java はさらにカスタマイズ可能な区切りを許し、区切り文字そのものを文字列内に含められるようにしています。
複数行文字列リテラルが示した方向性
SE-0168: Multi-Line String Literals は、改行のエスケープなしで複数行の文字列リテラルを書く手段を導入しただけでなく、Swift がカスタムの区切りへ向かう方向を先取りしていました。複数行文字列は """ で開始・終了するため、個々の引用符や改行をエスケープなしで書けます。たとえば次のようなエスケープだらけのリテラルが、
"\"Either it brings tears to their eyes, or else -\"\n\n\"Or else what?\" said Alice..."
""" で囲むことで、引用符と改行のエスケープが消えて読みやすくなります。
"""
"Either it brings tears to their eyes, or else -"
"Or else what?" said Alice...
"""
複数行文字列は補間・Unicode 挿入などの機能をすべて保ったままです。この体験が、Swift の「raw」文字列がどうあるべきかの基準になりました。
設計が固まるまで
SE-0200 は 2018 年 3 月に最初のレビューに入りました。当初の設計は単純に r 接頭辞を付けるものでしたが、「r"..." という構文が言語の他の部分になじまない」「ユースケースを十分にカバーできていない」といった理由でコミュニティの支持を得られず、4 月に差し戻されました。
設計を練り直す中で着目したのが Rust です。Rust は raw string にカスタマイズ可能な区切りを使い、r#""#、r##""## のように # の数で区切りを調整できます。文字列内に区切りと衝突する内容が含まれても、# を増やせば終端を調整できる仕組みです。SE-0200 は改訂で r を落とし、この Rust 流の # を両端に付ける設計を採用しました。
ここで「カスタム区切りは単なる raw string 以上の力を持つ」という気づきが生まれます。raw string は定義上エスケープを使わないため、補間が使えなくなります。そこで co-author の Brent Royal-Gordon が、Rust 由来の構文を取り入れつつエスケープへのアクセスを残すというアイデアを示しました。これにより SE-0200 は、raw string の力と Swift の補間の便利さを両立する設計になりました。
どう動くのか: # でエスケープも変わる
SE-0200 は、文字列リテラルの両端にカスタム区切りを追加すると同時に、エスケープトークンを単なるバックスラッシュから # 付きのものに変えます。"" 文字列ではエスケープは \、#""# では \#、##""## では \## です。エスケープがこの # 付きトークンに一致したときだけ、エスケープとして解釈されます。
"\(thisInterpolates)"
#"\(thisDoesntInterpolate) \#(thisInterpolates)"#
##"\(thisDoesntInterpolate) \#(thisDoesntInterpolate) \##(thisInterpolates)"##
"\n" // 改行
#"\n"# // バックスラッシュと n
#"\#n"# // 改行
エスケープと解釈されてほしくない内容が出てきたら、内容が解釈されなくなるまで区切りの # を増やせます。この機能が必要になることはまれですが、# を 1 つか 2 つ付けるだけで、文字列の一部では補間を有効に、別の部分では無効にできます。
コードでの使い方
Swift 5 では、次のリテラルはいずれも文字列 “Hello” を表します。
let u = "Hello" // # なし
let v = #"Hello"# // # ひとつ
let w = ####"Hello"#### // # 多数
let x = "\("Hello")" // 補間
let y = #"\#("Hello")"# // # 付きの補間
let z = """ // 複数行
Hello
"""
let a = #""" // # 付きの複数行
Hello
"""#
使い方のルールは次のとおりです。
- 文字列リテラルの前後で
#の数を一致させます。数は 0 でも 1 でも好きなだけでもよいですが、ほとんどの場合「0」か「1」が正解です。 #を使うときは、エスケープを単一のバックスラッシュから、同じ数の#を挟んだバックスラッシュに変えます。##"Hello"##なら\##です。- 終端の区切りに一致しないものはすべて文字列の一部です。複数行文字列に
"""をエスケープなしで含めたいときは、#を足して区切りを変えます。 - 必要な結果が得られる最小限の
#を使います。0 が最良、1 で十分、2 つ以上はごくまれです。
これにより、エスケープ済みの JSON を含むネットワークコードや、バックスラッシュの多いコンテンツ、コード生成ツールの出力などを、補間やエスケープの便利さを犠牲にせず、そのまま貼り付けて使えるようになります。エスケープのごたごたが減り、読みやすく、コピー&ペーストしやすいコードになります。
関連リンク
- SE-0200: 生テキストをサポートするため String Literals の区切りを強化する — この機能の仕様を定めた元の Proposal のダイジェスト
- SE-0168: Multi-Line String Literals — 複数行文字列リテラルを導入し、カスタム区切りへの道を開いた Proposal