Scala 3 — 书籍

字符串插值

语言

简介

字符串插值提供了一种在字符串中使用变量的方法。例如

val name = "James"
val age = 30
println(s"$name is $age years old")   // "James is 30 years old"

使用字符串插值包括在字符串引号前放置一个 s,并在任何变量名前加上一个 $ 符号。

其他插值器

你放在字符串前面的 s 只是 Scala 提供的一个可能的插值器。

Scala 提供了三种开箱即用的字符串插值方法:sfraw。此外,字符串插值器只是一个特殊方法,因此可以定义你自己的插值器。例如,一些数据库库定义了一个 sql 插值器,该插值器返回一个数据库查询。

s 插值器(s 字符串)

在任何字符串字面量前加上 s 允许直接在字符串中使用变量。你已经在这里看到一个示例

val name = "James"
val age = 30
println(s"$name is $age years old")   // "James is 30 years old"

这里,字符串中的 $name$age 占位符分别被调用 name.toStringage.toString 的结果替换。 s 字符串将访问当前范围内所有变量。

虽然这看起来很明显,但这里需要注意的是,字符串插值不会在普通字符串字面量中发生

val name = "James"
val age = 30
println("$name is $age years old")   // "$name is $age years old"

字符串插值器还可以接受任意表达式。例如

println(s"2 + 2 = ${2 + 2}")   // "2 + 2 = 4"
val x = -1
println(s"x.abs = ${x.abs}")   // "x.abs = 1"

任何任意表达式都可以嵌入 ${} 中。

对于一些特殊字符,在嵌入字符串中时有必要对其进行转义。要表示实际的美元符号,可以将其加倍 $$,如下所示

println(s"New offers starting at $$14.99")   // "New offers starting at $14.99"

双引号也需要转义。这可以通过使用三重引号来完成,如下所示

println(s"""{"name":"James"}""")     // `{"name":"James"}`

最后,所有多行字符串文字也可以进行插值

println(s"""name: "$name",
           |age: $age""".stripMargin)

将按如下方式打印

name: "James"
age: 30

f 插值器(f 字符串)

在任何字符串文字前加上 f 允许创建简单的格式化字符串,类似于其他语言中的 printf。在使用 f 插值器时,所有变量引用后面都应跟一个 printf 样式的格式字符串,例如 %d。我们来看一个例子

val height = 1.9d
val name = "James"
println(f"$name%s is $height%2.2f meters tall")  // "James is 1.90 meters tall"

f 插值器是类型安全的。如果您尝试传递仅适用于整数的格式字符串但传递了一个双精度数,编译器将发出错误。例如

val height: Double = 1.9d

scala> f"$height%4d"
<console>:9: error: type mismatch;
  found   : Double
  required: Int
            f"$height%4d"
              ^
val height: Double = 1.9d

scala> f"$height%4d"
-- Error: ----------------------------------------------------------------------
1 |f"$height%4d"
  |   ^^^^^^
  |   Found: (height : Double), Required: Int, Long, Byte, Short, BigInt
1 error found

f 插值器利用了 Java 提供的字符串格式实用程序。在 % 字符之后允许的格式在 Formatter javadoc 中进行了概述。如果变量定义后没有 % 字符,则假定格式化程序为 %sString)。

最后,与 Java 中一样,使用 %% 在输出字符串中获取一个文字 % 字符

println(f"3/19 is less than 20%%")  // "3/19 is less than 20%"

raw 插值器

原始插值器类似于 s 插值器,但它不会对字符串中的文字进行转义。这是一个经过处理的字符串示例

scala> s"a\nb"
res0: String =
a
b

此处 s 字符串插值器将字符 \n 替换为回车符。 raw 插值器不会这样做。

scala> raw"a\nb"
res1: String = a\nb

当您希望避免将 \n 等表达式转换为回车符时,原始插值器很有用。

除了三个默认字符串插值器之外,用户还可以定义自己的插值器。

高级用法

Scala 将文字 s"Hi $name" 解析为已处理的字符串文字。这意味着编译器对此文字进行了一些额外处理。已处理字符串和字符串插值的具体内容在 SIP-11 中进行了描述,但这里有一个快速示例来帮助说明它们的工作原理。

自定义插值器

在 Scala 中,所有已处理的字符串文字都是简单的代码转换。每当编译器遇到形式为

id"string content"

的已处理字符串文字时,它会将其转换为 StringContext 实例上的方法调用 (id)。此方法也可以在隐式作用域中使用。要定义我们自己的字符串插值,我们需要创建一个隐式类 (Scala 2) 或 extension 方法 (Scala 3),为 StringContext 添加一个新方法。

作为一个简单的示例,假设我们有一个简单的 Point 类,并希望创建一个自定义插值器,将 p"a,b" 转换为 Point 对象。

case class Point(x: Double, y: Double)

val pt = p"1,-2"     // Point(1.0,-2.0)

我们将通过首先使用类似于以下内容实现 StringContext 扩展来创建一个自定义 p 插值器

implicit class PointHelper(val sc: StringContext) extends AnyVal {
  def p(args: Any*): Point = ???
}

注意:在 Scala 2.x 中扩展 AnyVal 以防止每次插值时进行运行时实例化非常重要。有关更多信息,请参阅 值类 文档。

extension (sc: StringContext)
  def p(args: Any*): Point = ???

一旦此扩展处于作用域中,并且 Scala 编译器遇到 p"some string",它将处理 some string 以将其转换为字符串标记和字符串中每个嵌入变量的表达式参数。

例如,p"1, $someVar" 将转换为

new StringContext("1, ", "").p(someVar)

然后使用隐式类将其重写为以下内容

new PointHelper(new StringContext("1, ", "")).p(someVar)
StringContext("1, ","").p(someVar)

因此,处理过的 String 的每个片段都暴露在 StringContext.parts 成员中,而字符串中的任何表达式值都传递到方法的 args 参数中。

示例实现

我们的 Point 插值器方法的一个简单实现可能如下所示,尽管更复杂的方法可能会选择对字符串 parts 和表达式 args 的处理有更精确的控制,而不是重复使用 s 插值器。

implicit class PointHelper(val sc: StringContext) extends AnyVal {
  def p(args: Double*): Point = {
    // reuse the `s`-interpolator and then split on ','
    val pts = sc.s(args: _*).split(",", 2).map { _.toDoubleOption.getOrElse(0.0) }
    Point(pts(0), pts(1))
  }
}

val x=12.0

p"1, -2"        // Point(1.0, -2.0)
p"${x/5}, $x"   // Point(2.4, 12.0)
extension (sc: StringContext)
  def p(args: Double*): Point = {
    // reuse the `s`-interpolator and then split on ','
    val pts = sc.s(args: _*).split(",", 2).map { _.toDoubleOption.getOrElse(0.0) }
    Point(pts(0), pts(1))
  }

val x=12.0

p"1, -2"        // Point(1.0, -2.0)
p"${x/5}, $x"   // Point(2.4, 12.0)

虽然字符串插值器最初用于创建某种形式的 String,但如上所示,使用自定义插值器可以实现强大的语法简写,并且社区已经迅速将此语法用于 ANSI 终端颜色扩展、执行 SQL 查询、魔术 $"identifier" 表示等内容。

此页面的贡献者