原始字串常值
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在的相關
總結
允許以至少三個 """
字元(但不含最大值)開頭的新字串常值形式,選擇性地後面接著 new_line
、字串的內容,然後以常值開頭的相同引號數結束。 例如:
var xml = """
<element attr="content"/>
""";
因為巢狀內容本身可能想要使用 """
所以開始/結束分隔符可能更久,如下所示:
var xml = """"
Ok to use """ here
"""";
為了讓文字易於閱讀,並允許開發人員在程式碼中喜歡的縮排,這些字串文本在產生最終文本值時,會自然移除最後一行指定的縮排。 例如,一種形式的常量:
var xml = """
<element attr="content">
<body>
</body>
</element>
""";
將包含下列內容:
<element attr="content">
<body>
</body>
</element>
這可讓程式碼看起來自然,同時仍能產生所需的常值,若這過程需要使用特製字串操作程序,則可避免增加運行時成本。
如果不需要縮排行為,也可以非常簡單地停用,如下所示:
var xml = """
<element attr="content">
<body>
</body>
</element>
""";
也支援單行表單。 其開頭至少為三個 """
個字元(但沒有最大值),字串的內容(不能包含任何 new_line
字元),然後以常值開頭的相同引號數結束。 例如:
var xml = """<summary><element attr="content"/></summary>""";
也支援插入的原始字串。 在此情況下,字串會指定開始插補所需的大括號數目(由常值開頭出現的貨幣符號數目所決定)。 任何大括號序列的括號數目少於那個數量時,都會被視為內容。 例如:
var json = $$"""
{
"summary": "text",
"length" : {{value.Length}},
};
""";
動機
C# 缺乏建立簡單字串常值的一般方法,可以有效地包含任何任意文字。 現今的所有 C# 字串文字形式都需要某種形式的跳脫,以防內容使用了某些特殊字元(當使用分隔符時尤其如此)。 這可以防止字面值容易包含其他語言的內容(例如 XML、HTML 或 JSON 字面值)。
目前在 C# 中形成這些常值的所有目前方法,一律會強制用戶手動逸出內容。 此時的編輯可能會非常惱火,因為無法避免逸出,而且必須在內容中出現時處理。 對於regex來說,這特別痛苦,尤其是在它們包含引號或反斜杠時。 即使是逐字(@""
)字串,引號本身也必須進行跳脫,導致 C# 和正則表達式的混合交錯。
{
和 }
在插補($""
)字串中同樣令人沮喪。
問題的癥結在於,我們所有的字串都有固定的開始/結束分隔符。 只要發生這種情況,我們一律必須有逸出機制,因為字串內容可能需要在其內容中指定結束分隔符。 這特別有問題,因為分隔符 "
在許多語言中非常常見。
為了解決這個問題,此提案允許彈性的開始和結束分隔符,以便一律以不會與字串內容衝突的方式進行。
目標
- 提供一種機制,可讓使用者 所有 字串值,而不需要 任何 逸出序列。 由於所有字串都必須可表示,而不使用逸出序列,因此用戶必須一律能夠指定保證不會與任何文字內容相衝突的分隔符。
- 以相同方式支援插補。 如上所述,因為 所有 字串都必須可表示,而不需逸出,因此用戶必須一律可以指定一個保證不會與任何文字內容相撞的
interpolation
分隔符。 重要的是,使用我們 插補的語言 分隔符({
和}
)應該感覺一流的,而不是痛苦的使用。 - 多行字串文字在程式碼中看起來應該很清晰,而且不應該讓編譯單位內的縮排看起來很奇怪。 重要的是,本身沒有縮排的文字值不應強制佔用文件的第一列,因為這樣會破壞程式碼的流暢性,並且看起來與周圍的程式碼沒有對齊。
- 此行為應該很容易覆蓋,同時使文本內容保持清晰且易於閱讀。
- 對於本身不包含
new_line
且不以引號("
)字符開頭或結尾的所有字串,應該可以在單行上直接表示該字串常量本身。- 或者,使用額外的複雜度,我們可以精簡此內容,指出:對於本身不包含
new_line
的所有字串(但可以以引號"
字元開頭或結尾),應該可以代表單行上的字元串常值本身。 如需詳細資訊,請參閱<>一節中的擴充提案。
- 或者,使用額外的複雜度,我們可以精簡此內容,指出:對於本身不包含
詳細設計(非插補案例)
我們將新增一個 string_literal
生產任務,形式如下:
string_literal
: regular_string_literal
| verbatim_string_literal
| raw_string_literal
;
raw_string_literal
: single_line_raw_string_literal
| multi_line_raw_string_literal
;
raw_string_literal_delimiter
: """
| """"
| """""
| etc.
;
raw_content
: not_new_line+
;
single_line_raw_string_literal
: raw_string_literal_delimiter raw_content raw_string_literal_delimiter
;
multi_line_raw_string_literal
: raw_string_literal_delimiter whitespace* new_line (raw_content | new_line)* new_line whitespace* raw_string_literal_delimiter
;
not_new_line
: <any unicode character that is not new_line>
;
raw_string_literal
的結束分隔符必須符合起始分隔符。 因此,如果起始分隔符是 """""
結束分隔符也必須是。
上述 raw_string_literal
文法應解譯為:
- 它從至少三個引號開始(但沒有引號的上限)。
- 然後,內容會在開始引號所在的同一行繼續。 相同行上的這些內容可以是空白或非空白。 'blank' 是 '全由空白組成' 的同義詞。
- 如果同一行上的內容不是空白的,則不能有其他內容跟隨。 換句話說,字面值必須在同一行以相同數量的引號結束。
- 如果相同行的內容是空白的,則常值可以繼續使用
new_line
並在此之後添加若干內容行和new_line
。- 內容行是
new_line
以外的任何文字。 - 然後,它會以
new_line
若干數目(可能為零)的whitespace
結尾,並包含與字面量開頭相同數目的引號。
- 內容行是
原始字串常值
開始和結束 raw_string_literal_delimiter
之間的部分用以下列方式來形成 raw_string_literal
的值:
- 在
single_line_raw_string_literal
的情況下,常值將完全是開始和結束raw_string_literal_delimiter
之間的內容。 - 在
multi_line_raw_string_literal
的情況下,whitespace* new_line
的初始和new_line whitespace*
的結尾不屬於字串的值。 不過,whitespace*
終端機前面的最後一個raw_string_literal_delimiter
部分會被視為「縮排空格符」,並會影響其他行的解譯方式。 - 為了得到最終值,需要遍歷
(raw_content | new_line)*
序列,然後執行以下動作:- 如果是
new_line
,則new_line
的內容會新增至最終字串值。 - 如果不是「空白」
raw_content
(亦即not_new_line+
包含非whitespace
字元):- 「縮排空白位元」必須是
raw_content
的前綴。 否則為錯誤。 - 「縮排空格符」會從
raw_content
的開頭去除,剩餘部分會新增至最終的字串值。
- 「縮排空白位元」必須是
- 如果是『空白』
raw_content
(即not_new_line+
完全是whitespace
):- 「縮排空格符」必須是
raw_content
的前缀,或raw_content
必須是「縮排空格符」的前缀。 否則為錯誤。 - 由於 「縮排空格符」大部分會從
raw_content
開頭移除,且任何餘數會新增至最終字串值。
- 「縮排空格符」必須是
- 如果是
澄清:
single_line_raw_string_literal
無法代表具有new_line
值的字串。single_line_raw_string_literal
不會參與「縮排空白字符」去除。 其值一律是開頭和結尾分隔符之間的確切字元。因為
multi_line_raw_string_literal
會忽略最後一行的最後一個new_line
,下列代表字串沒有起始new_line
,也沒有結束的new_line
。
var v1 = """
This is the entire content of the string.
""";
這會維持對稱性,就像忽略起始 new_line
的做法一樣,並且也提供了一種統一的方法,以確保「縮排空白」可以隨時調整。 若要表示末端為 new_line
的字串,必須另外提供一行,如下所示:
var v1 = """
This string ends with a new line.
""";
single_line_raw_string_literal
不能代表以引號開頭或結尾的字串值("
),不過這份提案在Drawbacks
區段中提供了一種增強方案,說明如何支援這種情況。multi_line_raw_string_literal
會從初始whitespace* new_line
之後的raw_string_literal_delimiter
開始。 分隔符之後的整個內容都被完全忽略,在判斷字串的值時不會被使用。 這可讓機制指定一個raw_string_literal
,其內容本身以"
字元開頭。 例如:
var v1 = """
"The content of this string starts with a quote
""";
-
raw_string_literal
也可以代表以引號結尾的內容("
)。 這是合理的,因為終止分隔符必須位於單獨的一行。 例如:
var v1 = """
"The content of this string starts and ends with a quote"
""";
var v1 = """
""The content of this string starts and ends with two quotes""
""";
- 「空白」
raw_content
必須是「縮排空格符」的前綴,或者「縮排空格符」必須是其前綴,以幫助確保混合空格符的混淆情況不會發生,特別是在該行的處理方式尚不明確的情況下。 例如,下列案例是非法的:
var v1 = """
Start
<tab>
End
""";
此處的「縮排空格符」是九個空格符,但「空白符」
raw_content
開頭不是該前綴。 目前還沒有明確的答案,即應該如何處理<tab>
行。 應該忽略嗎? 應該與.........<tab>
相同嗎? 因此,使其成為非法似乎是避免混亂的最清楚方法。不過,下列案例是合法的,而且代表相同的字串:
var v1 = """
Start
<four spaces>
End
""";
var v1 = """
Start
<nine spaces>
End
""";
在這兩種情況下,「縮排空格」將會是九個空格。 在這兩種情況下,我們將盡可能移除該前置詞,導致每個案例中的「空白」raw_content
是空的(不計算每個 new_line
)。 這讓使用者在複製/貼上或編輯這些行時,不必看到這些行上的空白,也不必因此困擾。
- 不過在這種情況下:
var v1 = """
Start
<ten spaces>
End
""";
「縮排空白字符」仍將是九個空格。 在這裡,我們會盡可能移除「縮排空格符」,而「空白」raw_content
會對最終內容貢獻一個空格。 這樣做允許在某些需要保留空白的內容行上保留空白。
- 在技術上不合法:
var v1 = """
""";
這是因為原始字串的開頭必須有一個 new_line
(它確實如此),但結尾也必須有 new_line
(它沒有)。 最低法律 raw_string_literal
為:
var v1 = """
""";
不過,這個字串顯得毫無趣味,因為它相當於 ""
。
縮排範例
「縮排空格符」演算法可以在多個輸入資料上進行可視化,如下所示。 下列範例使用垂直橫條字元 |
來說明結果原始字串中的第一個數據行:
範例 1 - 標準案例
var xml = """
<element attr="content">
<body>
</body>
</element>
""";
解譯為
var xml = """
|<element attr="content">
| <body>
| </body>
|</element>
""";
範例 2 - 在與內容相同的行上結束分隔符。
var xml = """
<element attr="content">
<body>
</body>
</element>""";
這是非法的。 最後一個內容行的結尾必須是 new_line
。
範例 3 - 開始分隔符之前的結束分隔符
var xml = """
<element attr="content">
<body>
</body>
</element>
""";
解譯為
var xml = """
| <element attr="content">
| <body>
| </body>
| </element>
""";
範例 4 - 開始分隔符之後的結束分隔符
var xml = """
<element attr="content">
<body>
</body>
</element>
""";
這是非法的。 內容行必須以「縮排空格符」開頭
範例 5 - 空白行
var xml = """
<element attr="content">
<body>
</body>
</element>
""";
解譯為
var xml = """
|<element attr="content">
| <body>
| </body>
|
|</element>
""";
範例 6 - 空格符小於前置符的空白行(點代表空格)
var xml = """
<element attr="content">
<body>
</body>
....
</element>
""";
解譯為
var xml = """
|<element attr="content">
| <body>
| </body>
|
|</element>
""";
範例 7 - 空白行的空白多於前綴(點代表空格)
var xml = """
<element attr="content">
<body>
</body>
..............
</element>
""";
解譯為
var xml = """
|<element attr="content">
| <body>
| </body>
|....
|</element>
""";
詳細設計(插補案例)
目前透過使用 $"..."
字元啟動 {
,以及使用 interpolation
逸出序列來插入實際的左大括號字元,支援一般插補字串中的插補(例如 {{
)。 使用相同的機制將違反這一提案的目標 『1』 和 『2』。 以 {
為核心字元的語言(例如 JavaScript、JSON、Regex,甚至內嵌的 C#)現在需要進行轉義,這樣的要求與原始字串字面量的用途相違背。
為了支援插補,我們會以不同於一般 $"
插補字串的方式來介紹它們。 具體而言,interpolated_raw_string_literal
會以一些 $
字元開頭。 這些計數顯示在常值的內容中,需要多少個 {
(和 }
)字元來界定 interpolation
。 重要的是,對於花括號,仍然沒有逃逸機制。 相反地,就像引號("
)一樣,字面值本身能確保它指定的插值分隔符號,不會與字串的其他內容衝突。 例如,可以撰寫包含插補點洞的 JSON 常值,如下所示:
var v1 = $$"""
{
"orders":
[
{ "number": {{order_number}} }
]
}
"""
在這裡,{{...}}
符合由 $$
分隔符前置詞指定的兩個大括弧的所需數量。 在單一 $
的情況下,這意味著內插的方式與一般插值字串常數 {...}
相同。 重要的是,這表示具有 N
$
字元的插補常值可以有一連串的 2*N-1
大括號(在數據列中為相同類型)。 最後的 N
個大括號會開始(或結束)一次內插,而其餘的 N-1
個大括號則只會作為內容出現。 例如:
var v1 = $$"""X{{{1+1}}}Z""";
在此情況下,內部的兩個 {{
和 }}
大括弧屬於內插,而外部的單一大括弧只是內容。 因此,上述字串相當於內容 X{2}Z
。 具有 2*N
(或更多) 的括號一律視為錯誤。 若要將長括弧序列當做內容,必須據以增加 $
個字元的數目。
插入的原始字串常數定義為:
interpolated_raw_string_literal
: single_line_interpolated_raw_string_literal
| multi_line_interpolated_raw_string_literal
;
interpolated_raw_string_start
: $
| $$
| $$$
| etc.
;
interpolated_raw_string_literal_delimiter
: interpolated_raw_string_start raw_string_literal_delimiter
;
single_line_interpolated_raw_string_literal
: interpolated_raw_string_literal_delimiter interpolated_raw_content raw_string_literal_delimiter
;
multi_line_interpolated_raw_string_literal
: interpolated_raw_string_literal_delimiter whitespace* new_line (interpolated_raw_content | new_line)* new_line whitespace* raw_string_literal_delimiter
;
interpolated_raw_content
: (not_new_line | raw_interpolation)+
;
raw_interpolation
: raw_interpolation_start interpolation raw_interpolation_end
;
raw_interpolation_start
: {
| {{
| {{{
| etc.
;
raw_interpolation_end
: }
| }}
| }}}
| etc.
;
上述與 raw_string_literal
的定義類似,但有一些重要的差異。
interpolated_raw_string_literal
應解譯為:
- 它從至少一個美元記號(但沒有上限)開始,然後三個引號(也沒有上限)。
- 然後內容會繼續在起始引號的同一行中。 相同行上的此內容可以是空白或非空白。 'blank' 是 '全由空白組成' 的同義詞。
- 如果同一行的內容不是空白,則不能有其他內容跟在後面。 換句話說,字面值必須在同一行以相同數量的引號結束。
- 如果相同行的內容是空白的,則常值可以繼續使用
new_line
並在此之後添加若干內容行和new_line
。- 內容行是
new_line
以外的任何文字。 - 內容行可以在任何位置包含多個
raw_interpolation
。raw_interpolation
的開頭必須有與常值開頭的貨幣符號數目相等的左大括弧({
)。 - 如果「縮排空白字符」不是空白,則
raw_interpolation
不能緊接在new_line
之後。 -
raw_interpolation
將依照 第12.8.3條中指定的一般規則。 任何raw_interpolation
都必須以與美元符號和左大括弧相同的右大括弧(}
)結尾。 - 任何
interpolation
本身都可以以與一般interpolation
中的verbatim_string_literal
相同方式包含新行(@""
)。 - 然後,它會以
new_line
若干數目(可能為零)的whitespace
結尾,並包含與字面量開頭相同數目的引號。
- 內容行是
計算插值字串的規則與一般 raw_string_literal
相同,但已更新以處理包含 raw_interpolation
的行。 構建字串值的方式相同,只是插值孔會在執行時以這些表達式生成的值取代。 如果 interpolated_raw_string_literal
轉換成 FormattableString
,則插值的數值會依各自的順序傳遞至 arguments
陣列,完成 FormattableString.Create
。 在從所有行中去除 的「縮排空格符」後,
上述規格中有模棱兩可。 具體來說,當文字中的區段 {
與插值中的 {
相鄰時。 例如:
var v1 = $$"""
{{{order_number}}}
"""
這可以解譯為:{{ {order_number } }}
或 { {{order_number}} }
。 不過,由於前者是非法的(沒有 C# 運算式可以從 {
開始),所以以這種方式解譯是毫無意義的。 因此,我們會以後一種方式解讀,其中最內層的 {
和 }
大括號形成插值,任何最外層的括號則構成文本。 未來,如果語言支持任何用大括弧括住的表達式,這可能是個問題。 不過,在此情況下,建議撰寫這類案例,如下所示:{{({some_new_expression_form})}}
。 在這裡,括弧有助於將表達式部分從整個常值或內插中區分開來。 這在優先處理上已有先例,必須將三元條件表達式包裹起來,以避免與插值的格式/對齊規範衝突(例如 {(x ? y : z)}
)。
缺點
原始字串常值會將更複雜的內容新增至語言。 我們已經有許多字串文字形式,適用於許多用途。
""
字串、@""
字串和 $""
字串已經有大量的能力和彈性。 但他們都缺乏提供永遠不需要逸出的原始內容的方法。
上述規則不支援 4.a案例:
- ...
- 或者,使用額外的複雜度,我們可以精簡此內容,指出:對於本身不包含
new_line
的所有字串(但可以以引號"
字元開頭或結尾),應該可以代表單行上的字元串常值本身。
- 或者,使用額外的複雜度,我們可以精簡此內容,指出:對於本身不包含
這是因為我們沒有辦法知道開始或結束引號 ("
) 應該屬於內容,而不是分隔符本身。 不過,如果這是我們想要支援的一個重要案例,我們可以新增平行 '''
建構來與 """
形式一起。 透過該平行建構,以 "
開頭和結尾的單一行字串,可以輕易地撰寫為 '''"This string starts and ends with quotes"'''
,以及平行建構 """'This string starts and ends with apostrophes'"""
。 這項支援可以幫助以視覺區分引號字元,特別是在內嵌主要使用一種引號的語言時,這將比其他情況更有幫助。
替代方案
https://github.com/dotnet/csharplang/discussions/89 涵蓋此處的許多選項。 替代方案很多,但我覺得這些方案過於複雜且在人因工程上表現不佳。 這種方法選擇簡單的方法,讓您不斷增加開始/結束引號長度,直到不再與字串內容衝突。 它也可讓您撰寫的程式代碼看起來很縮排,同時仍然產生縮排常值,這是大部分程式代碼想要的。
不過,其中一個最有趣的潛在變化是使用 `
(或 ```
)作為這些原始字串字面值的分隔符。 這會有數個優點:
- 它會避免字串開頭或結尾為引號的所有問題。
- Markdown 看起來很熟悉。 雖然這本身可能不是件好事,因為使用者可能會期待 Markdown 解析。
- 在大多數情況下,原始字串文字只需要用單一字元作為開頭和結尾,而只有在非常少見的情況下,內容包含反引號時才需要使用多個字元。
- 在未來用
```xml
來擴展功能會很自然,這就像是「Markdown」一樣。 當然,這也是"""
形式。
不過,總的來說,這裡的凈利益似乎很小。 遵循 C# 的歷史,我認為 "
應該繼續作為 string literal
的分隔符,正如在 @""
和 $""
中一樣。
設計會議
待討論的問題 已解決的問題:
- [x] 我們應該有單行表單嗎? 從技術上說,我們可以不這麼做。 但是,這表示不包含換行的簡單字串一律至少需要三行。 我認為我們應該非常重量級,迫使單線結構成為三行,只是為了避免逃跑。
設計決策:是的,我們將有單行表單。
- [x] 我們應該要求多行 必須從新的一行開始 嗎? 我想我們應該。 它也使我們能夠在未來支持諸如
"""xml
之類的事情。
設計決策:是的,我們將要求多行文字必須從新的一行開始
- [x] 自動縮進是否應該執行? 我想我們應該。 它讓程式代碼看起來更愉快。
設計決策:是的,自動退縮將會完成。
- [x] 我們應該限制常見空白字符以避免混合不同空白類型嗎? 我不認為我們應該。 事實上,有一種常見的縮排策略,叫做「使用索引標籤進行縮排,使用空白鍵進行對齊」。 在開始分隔符不位於製表位上的情況下,使用這個來使結束分隔符與開始分隔符對齊是非常自然的。
設計決策:我們不會對混合空格符有任何限制。
- [x] 我們應該使用其他東西作為柵欄嗎?
`
會比對 Markdown 語法,這表示我們不需要一律使用三個引號來啟動這些字串。 只要一個就能應付一般情況。
設計決策:我們將使用 """
- [x] 我們是否應該要求分隔符的引號比字串值中最長的引號序列還要多? 從技術上看,這不是必要專案。 例如:
var v = """
contents"""""
"""
這是字串,"""
做為分隔符。 數個社群成員表示這是令人困惑的,因此,在這樣的案例中,我們應該要求分隔符一律具有更多字元。 接下來是:
var v = """"""
contents"""""
""""""
設計決策:是,分隔符必須比字串本身的任何引號序列還要長。