簡介
字串內插提供一種在字串中使用變數的方法。例如
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
等運算式轉換為換行字元時,原始內插器會很有用。
除了三個預設字串內插器之外,使用者可以定義自己的內插器。
進階用法
文字 s"Hi $name"
由 Scala 解析為已處理的字串文字。這表示編譯器會對此文字執行一些額外的處理。已處理字串和字串內插的具體說明在 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)
因此,處理後的字串的每個片段都顯示在 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)
雖然字串內插器最初用於建立某種形式的字串,但如上所述,使用自訂內插器可以允許強大的語法簡寫,而且社群已經迅速地將此語法用於 ANSI 終端機色彩擴充、執行 SQL 查詢、神奇 $"identifier"
表示法,以及許多其他用途。