Proposal 背后的故事 — SE-0200 增强字符串字面量分隔符以支持原始文本

SE-0200 增强字符串字面量分隔符以支持原始文本 的开发、改进和部署是一段漫长而令人惊讶的旅程。它以独特的 Swift 方式结束了对“原始字符串”的处理,重点是为字符串字面量和转义序列添加自定义分隔符。

这篇文章讨论了什么是原始字符串,Swift 如何设计其对这项技术的应用,以及如何在您的代码中使用这个新的 Swift 5 功能。

转义序列

转义序列是以反斜杠开头的组合,例如 \\\"\u{n},它们包含了在普通字符串字面量中难以表达的字符。Swift 转义序列包括:

例如,字符串字面量 "hello\n\n\tworld" 由三行组成,第一行是 “hello”,第三行是 “world”。“world” 缩进了单个制表符

hello

	world

相比之下,原始字符串忽略转义序列,并将所有内容视为字面字符。在原始字符串中,\n 表示反斜杠字符后跟字母 n,而不是换行符。此功能用于生成代码输出、处理正则表达式、使用应用内源代码(例如,交互式教学语言)以及预转义的特定领域内容(如 JSON 和 XML)的应用中。

原始字符串

许多语言都使用原始字符串,包括 C#、Perl、Rust、Python、Ruby 和 Scala。原始字符串不解释转义序列。其内容会持续到到达字符串的结束分隔符,这在不同语言中有所不同,如下表所示:

语法 语言
'Hello, world!' Bourne shell, Perl, PHP, Ruby, Windows PowerShell
q(Hello, world!) Perl(备选)
%q(Hello, world!) Ruby(备选)
@"Hello, world!" C#, F#
R"(Hello, world!)" C++11
r"Hello, world!" D, Python
r#"Hello, world!"# Rust
"""hello \' world"""raw"Hello, world!" Scala
`Hello, world!` D, Go, `…`
``...`` Java,任意数量的 `

大多数语言采用前缀(如 qRr)来指示原始内容。Rust 和 Java 更进一步,允许自定义分隔符。此功能允许分隔符的变体包含在字符串中,从而允许更具表现力的原始字符串内容。

多行 Swift 字符串

SE-0168 多行字符串字面量 不仅引入了一种创建包含多行且没有换行符转义符的字符串字面量的方法,还暗示了 Swift 语言在自定义分隔符方面的发展方向。由于多行字符串使用三个引号 """ 来开始和结束字面量,因此它们允许使用单个引号和换行符,而无需转义序列。在新系统中,此字面量

"\"Either it brings tears to their eyes, or else -\"\n\n\"Or else what?\" said Alice, for the Knight had made a sudden pause.\n\n\"Or else it doesn't, you know.\""

变成了这样

"""
    "Either it brings tears to their eyes, or else -"

    "Or else what?" said Alice, for the Knight had made a sudden pause.

    "Or else it doesn't, you know."
    """

在新语法中,引号和换行符反斜杠消失了。生成的字符串字面量清晰、可读且可检查。在引入新的分隔符和多行支持时,可以使用换行符和引号,而无需转义,这是朝着更好的字面量迈出的第一步。

多行字面量并没有失去 Swift 字符串的任何功能。它们支持转义,包括插值、Unicode 字符插入等等。同时,该功能为 Swift “原始” 字符串的外观设定了标准。

Swift 原始字符串:第一版

SE-0200 于 2018 年 3 月首次进入审核阶段。它的 初始设计 为单行和多行字符串添加了单个 r 前缀。社区不喜欢这种设计(“提议的 r"..." 语法与语言的其余部分不太协调”),并认为它不够广泛,无法支持足够多的用例。该提案于 2018 年 4 月 退回进行修订。现在是时候寻找更好的设计、更好的用例和更符合 Swift 风格的表达方式了。

重新审视设计包括广泛审查其他语言中的原始字符串,最终重点关注 Rust。Rust 不仅支持原始字符串,还使用可自定义的分隔符。您可以使用 r#""#r##""##r###""### 等创建原始字符串。您可以选择要填充字符串字面量每一侧的井号的数量。在不太可能的情况下,您需要在字符串中包含 "#,这通常会终止基本原始字符串,这些自定义分隔符可确保您可以添加第二个井号,从而允许您调整字符串的结尾方式。

是的,您极少需要超过一个井号,但 Rust 的设计考虑到了这种罕见性。它创建了一个可扩展和可自定义的系统,甚至可以覆盖最离奇的边缘情况。这种优势令人印象深刻,并且是 Swift 最终设计的核心。在其修订版中,SE-0200 放弃了 r(代表 “raw”),同时采用了 Rust 风格的可适应井号,分别位于字面量的两侧。与 Rust 中一样,每个 Swift 字符串字面量都必须在前后使用相同数量的井号,无论使用单行还是多行字符串。

在那时,灵感迸发,SE-0200 团队意识到自定义分隔符比普通的原始字符串更强大。

可自定义的分隔符

当使用更新后的原始字符串设计时,团队一次又一次地后悔失去了字符串插值。根据定义,原始字符串不使用转义序列。插值依赖于它们。SE-0200 的共同作者 Brent Royal-Gordon 灵光一闪,我们可以在保留对转义序列的访问权限的同时,融入 Rust 风格的语法。

SE-0200 没有创建原始字符串,而是引入了一些类似的东西:Swift 在多行字符串中首次遇到的备用分隔符和 Rust 的可自定义分隔符的混合体。通过将这种自定义扩展到转义序列,SE-0200 的设计继承了原始字符串的所有功能,以及 Swift 插值的便利性。

SE-0200 在每个字符串字面量的开头和结尾添加了自定义分隔符,并同步地将转义序列分隔符从简单的反斜杠自定义为用井号装饰的分隔符。此设计使转义序列与字符串字面量的井号数量相匹配。对于 "" 字符串,转义标记是 \。对于 #""#,它是 \#,对于 ##""##,它是 \##,依此类推。

通过添加转义序列——此修改支持所有转义序列,而不仅仅是插值——Swift 的 #-注释字符串不再是“原始”的。它们支持您在原始字符串中找到的相同功能,它们在很大程度上像原始字符串一样工作,但是该设计包含了转义,这意味着字面量不是原始的。如果您觉得有趣,您可以称它们为“半熟”字符串。

每当您包含会被识别为转义序列的内容时,都可以扩展分隔符井号的数量,直到不再解释内容为止。很少需要此功能,但当使用时,只需一两个井号就应该既支持字符串某些部分的插值,又禁止其他部分的插值

"\(thisInterpolates)"
#"\(thisDoesntInterpolate) \#(thisInterpolates)"#
##"\(thisDoesntInterpolate) \#(thisDoesntInterpolate) \##(thisInterpolates)"##

"\n" // new line
#"\n"# // backslash plus n
#"\#n"# // new line

在您的代码中采用 SE-0200 字符串

在 Swift 5 中,以下每个字面量都声明了字符串 “Hello”,即使它们使用了各种单行和多行样式

let u = "Hello" // No pounds
let v = #"Hello"# // One pound
let w = ####"Hello"#### // Many pounds
let x = "\("Hello")" // Interpolation
let y = #"\#("Hello")"# // Interpolation with pound
let z = """ // Multiline
    Hello
    """
let a = #""" // Multiline with pound
    Hello
    """#

规则如下:

借助 SE-0200,任何编写代码生成应用程序(如 PaintCode 或 Kite Compositor)、编写包含转义 JSON 的网络代码或包含大量反斜杠 ASCII 剪贴画的人都可以直接粘贴并开始使用。根据需要添加井号,而不会牺牲字符串插值或转义序列的便利性。

这些分隔符确保您的代码保持免受转义混乱的困扰。结果更简洁。它们更易于阅读和剪切/粘贴到您的代码库中。您将能够测试、重新配置和调整原始内容,而无需克服转义和取消转义的障碍,否则这些障碍会限制您的开发。

SE-0200 提案中阅读有关 Swift 新的自定义字符串分隔符的更多信息。它包括更多详细信息、许多示例,并探讨了经过考虑和拒绝的备选设计。

有问题吗?

请随时在 相关主题上在 Swift 论坛上发布有关这篇文章的问题。