简介
字符串插值提供了一种在字符串中使用变量的方法。例如
val name = "James"
val age = 30
println(s"$name is $age years old") // "James is 30 years old"
使用字符串插值包括在字符串引号前放置一个 s
,并在任何变量名前加上一个 $
符号。
其他插值器
你放在字符串前面的 s
只是 Scala 提供的一个可能的插值器。
Scala 提供了三种开箱即用的字符串插值方法:s
、f
和 raw
。此外,字符串插值器只是一个特殊方法,因此可以定义你自己的插值器。例如,一些数据库库定义了一个 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.toString
和 age.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 中进行了概述。如果变量定义后没有 %
字符,则假定格式化程序为 %s
(String
)。
最后,与 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"
表示等内容。