Swift 5 - 对 raw string 的支持

swift 发布于 2019年06月24日

Swift 5 在今年年初正式发布了, 这个年轻的语言现在也变得越来越完善了, 最近打算和大家聊聊它最新的改变.

什么是 raw string

作为开篇, 和大家介绍的第一个概念就是 Swift 5 对于 raw string 的支持. 那么首先说一下什么是 raw string. raw string 这个词翻译过来总觉得反而词不达意, 所以我们姑且用它本来的英文名字.

我们在 Swift 中声明一个字符串常量的时候, 有时候会用到一些特殊字符, 比如换行, 双引号, 反斜杠等等. 这些特殊字符在输入的时候需要进行所谓的转义:

"My \"app\"" // My "app"

上面这个字符串中的双引号, 换行, 用 \" 进行了转义. 这种字符串常量是我们常见的形式, 称作 conventional string, 和他对应的, 就是我们这里要说的 raw string. 顾名思义 raw string 不会对字符串常量中的任何内容进行转义, 你输入的什么最终的内容就是什么. 在 Swift 5 中, 你可以这样:

#"My "app""# // My "app"

我们先不要考虑两边的 #, 只看字符串的内容, 这里我们对 "app" 两边的双引号不需要进行转义, 也可以达到和上面同样的效果.

为什么要加入 raw string

前面给大家粗略的介绍了一下什么是 raw string, 那么我们再来说一下为什么要在 Swift 5 中加入对 raw string 的支持.

首先, raw string 并不是一个新概念, 它在很多其他的编程语言中已经有实现了, 比如:

python:

r"Hello, world!"

新版本的 java:

``...``

那么为什么要在 Swift 5 加入这样一个概念呢? 我们再来看一个例子:

"enum\\s+.+\\{.*case\\s+[:upper:]"

上面的字符串是一个正则表达式: enum\s+.+\{.*case\s+[:upper:] 可以看到正则表达式本身也包含 \ 这样的特殊字符, 但是 Swift 现有的字符串常量系统, 没有办法直接输入这些特殊字符, 必须通过转义的方式输入. 就像上面这个例子中一样, 反斜杠连续输入了两次, 因为它本身也需要转义.

再比如, 如果我们要在字符串常量中输入一个 JSON 数据:

"{ name: \"swift\"}"

JSON 数据中的双引号也需要转义, 因为它和用于表示字符串起始和结尾的双引号重复了. 类似的例子还有很多, 不一一列举了.

总的来说, 如果字符串中需要使用大量的转义序列, 它就会变得很难理解, 并且不易维护, 这也是 raw string 引入的最主要原因.

Swift 如何实现 raw string

前面介绍了 raw string 是什么, 以及为什么要实现它. 我们接着再看看 Swift 团队如何实现的它. Swift 在实现 raw string 的过程中, 借鉴了很多其他语言. 最终的灵感来自 Rust 语言.

Rust 通过这个语法来实现 raw string:

r"..."
r#"..."#
r##"..."##

以一个 r 开头, 后面是双引号. 你还可以在双引号的左右两边加上任意数量的 # 字符. 两边的 # 数量要相等. 加这些 # 字符能干什么呢, 比如你要在字符串中输入双引号, 那么就和 r"..." 这种语法的双引号冲突了, 编译器会把你输入的双引号当做字符串结尾. 这时候你使用 r#"..."# 这种格式, 就可以完美解决这个问题. 同理, 如果还遇到同样的冲突情况, 你可以输入更多的 #, 来解决冲突. 当然, 这种情况几乎不会发生.

Swift 5 正是基于 Rust 的这个灵感, 设计它自己的 raw string 语法. 虽然是基于 Rust, 但 Swift 5 最终的方案和它还是不一样的. 这经过了一系列有趣的思维过程. 大家如果感兴趣, 可以到 SE-0200 方案的主页一窥究竟:

https://github.com/apple/swift-evolution/blob/master/proposals/0200-raw-string-escaping.md#design

简单来说, Swift 5 采用了一种不同于 Rust 的方案, 它不像其他语言那样, 严格区分 raw string 和普通字符串. 在 Swift 5 中, 严格意义上依然只有一种字符串. 只是它的定界符,和转义符的解析规则发生了变化.

我们看看 Swift 5 中如何表示这段文字 my #1 "app":

print("my #1 \"app\"")
print(##"my #1 "app""##)

第一行代码使用的是我们之前的写法, 对字符串内的双引号进行了转义, 第二行就是 Swift 5 新引入的写法了, 和前面我们说的 Rust 的例子是不是很像, 因为字符串内部存在一个 # 字符. 所以我们这次在字符串的左右两边各加入了两个 # 符号.

还有一点差别, 就是 Swift 5 的这个语法中, 字符串前面不用像 Rust 那样,前缀一个 r -> r##"..."##.

这也是一个有意思的过程, Swift 社区觉得这个 r 有点不好看, 于是在语法中把它去掉了 :)

不是真正的 raw string

正如我们刚才说的, Swift 5 新的字符串语法不是严格意义上的 raw string. 所谓 raw string 的一个重要概念, 就是它里面的所有内容都不会被转义.

那么 Swift 团队所采用的不是严格意义上 raw string 又是什么鬼呢. 在设计这个语法过程中, 他们考虑到了 Swift 的一个重要特性, 就是变量插入. 经常做 Swift 开发的同学应该熟悉这个, 我们可以在字符串任意地方插入变量:

let name = "swift"
print("i like \(name)")

就像上面这样, 我们可以将变量,以及任何计算值, 直接插入到字符串中. 如果 Swift 采用了纯粹的 raw string 方案, 虽然可以避免诸如双引号, 反斜杠等这些特殊字符需要强制转义的问题, 但这同时也会舍弃 Swift 变量插入这一强大能力. 因为纯粹的 raw string 也同样不会解析这些. 比如上面这句 print("i like \(name)"), 如果是纯粹的 raw string, 那么他就会原样输出, 不会转换成 name 变量的值.

Swift 5 的方案

所以 Swift 5 中, 最终采用另一种方案, 通过可变的定界符, 来达到和 raw string 几乎一样的效果, 还保留了 Swift 变量插入的能力. 这一下子又出现好几个新词. 大家别急, 容我慢慢道来.

所谓的可变定界符, 其实就是字符串左右两边的符号, 比如双引号 "...", 我们之前使用 Swift 定义字符串的时候, 通过双引号来确定字符串的开始和结束. 这就是定界符.

可变定界符也不难理解, 我们刚才已经写过了:

"..."
#"..."#
##"...."##

上面这些, 在 Swift 5 中, 都是合法的字符串语法, 发现了吧, 字符串的开始和结尾不止用一个单引号可以表示, 还可以通过在左右两边添加任意数量的 # 符号, 来构成新的定界符. 前提是左右两边 # 符号的数量要相同.

大家想想, 定界符发生改变, 那么我们是不是可以直接在字符串输入双引号了呢, 就像我们前面做的那样:

print(##"my #1 "app""##)

我们上面这行代码, 因为定界符不再是双引号了, 所以我们在字符串中直接输入的双引号,不会和定界符发生冲突了. 相信敏感的朋友应该已经想到了, 虽然双引号不用转义了, 但是还有很多特殊字符呢. 如果 Swift 5 不是用的严格 raw string, 其他特殊字符怎么处理呢?

回想一下, 我们在字符串中插入特殊字符的时候, 需要使用反斜杠 \, 来表示这是一个转义的开始, 比如 \t, \n, \" 等等. 那么 Swift 5 这个新语法是怎么处理的呢, 其实当你在定义新的定界符的时候, 也同时改变了转义符号, 比如:

##"my #1 "app""##

还是这行代码, 在双引号的两边各加上了两个 # 号, 那么它的左右定界符就变成了 ##""##, 与此同时,这个字符串的转移符号, 也变成了 \##. 如果我们想在这样的字符串中插入变量的话, 可以这么做:

let name = "swift"
print(##"i like \##(name)"##)

之前的 \(name) 语法, 变成了 \##(name). 因为我们在左右两边插入了同样的 #. 这个规则对于我们常见的特殊字符也有效:

print(##"first line \##n second line"##)

这里的 \##n 等同于我们之前的 \n.

让我来做个总结吧

  1. Swift 5 新的语法, 可以让你通过在字符串两边加入任意数量的 # 符号, 来改变系统默认的字符串定界符, 比如 #"..."#, ##"..."##.
  2. 在改变定界符的同时, 也会根据你在两边加入的 # 的数量, 改变反斜杠转义符的写法. 你加入一个 # 符号, 转义符就是 '#', 加入两个,就是\## 以此类推.
  3. 只要字符串中的内容, 不和新的定界符冲突, 比如 ##", "## 并且不匹配到新的转义符, 比如 #, ## 等, 就会被原样输出.

我们来看几个例子吧:

// 1. 默认双引号定界符, 字符串内的双引号需要转义
print("my #1 \"app\"") // 输出: my #1 "app"

// 2. 自定义定界符, 加入两个 #, 双引号和定界符不再冲突, 不需转义
print(##"my #1 "app""##) // 输出: my #1 "app"

// 3. 自定义定界符, 默认的转义符 \ 无效,原样输出
print(##"\"\n"##) // 输出: \"\n

// 4. 自定义定界符, 使用了合规的转义符 \##, 转义
print(##"\##"\##n"##) // 输出: "<换行>
总结

做个总结吧, Swift 5 引入的不是严格的 raw string, 是通过改变字符串的定界符, 同时改变了转义符的解析规则. 来达到和 raw string 几乎相同的效果. 现在我们可以更轻松的在字符串常量中, 输入一段 JSON 数据:

print(#"{ name: "swift" }"#)

改变了定界符, 就不需要转义字符串中的双引号了. 同时, Swift 在实现和 raw string 同样效果的同时, 还保留了变量插入的能力, 我们还可以这样:

let launguage = "swift"
print(#"{ name: "\#(launguage)" }"#)

通过转义符 \#(), 我们依然可以向字符串插入变量. 并且我们可以通过加入任意数量的 # 符号, 来定义多种多样的定界符, 用来防止字符冲突. 就像官方主页上说的那样, 这种冲突的情况虽然很少见, 但是还为开发者提供了这个能力.

怎么样, 看过这篇, 是否对 Swift 5 又多了一份了解呢, 也欢迎大家一起讨论, 在下面留言.

为大家建了一个 Playground, 演示了这个新特性, 欢迎随时取阅:

https://github.com/swiftcafex/swift-5-travel/tree/master/1-raw-string/raw-string.playground


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe