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 等運算式轉換為換行字元時,原始內插器會很有用。

除了三個預設字串內插器之外,使用者可以定義自己的內插器。

進階用法

文字 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" 表示法,以及許多其他用途。

此頁面的貢獻者