Scala 3 — 書籍

隱式轉換

語言

隱式轉換是 Scala 的一項強大功能,讓使用者可以提供一個類型的參數,彷彿它是另一個類型,以避免樣板。

請注意,在 Scala 2 中,隱式轉換也用於提供封閉類別的其他成員(請參閱 隱式類別)。在 Scala 3 中,我們建議透過定義 延伸方法來解決此用例,而不是隱式轉換(儘管標準函式庫基於歷史原因仍依賴隱式轉換)。

範例

例如,考慮一個方法 findUserById,它採用 Long 類型的參數

def findUserById(id: Long): Option[User]

為簡潔起見,我們省略類型 User 的定義,這對我們的範例並不重要。

在 Scala 中,可以呼叫方法 findUserById,並使用類型為 Int 的引數,而不是預期的類型 Long,因為引數會隱式轉換為類型 Long

val id: Int = 42
findUserById(id) // OK

此程式碼不會因「類型不符:預期 Long,但找到 Int」等錯誤而無法編譯,因為有一個隱式轉換,將引數 id 轉換為類型 Long 的值。

詳細說明

本節說明如何定義和使用隱式轉換。

定義隱式轉換

在 Scala 2 中,從類型 S 到類型 T 的隱式轉換是由 隱式類別 T 定義的,它採用單一建構函數參數類型 S,一個函數類型 S => T隱式值,或一個可轉換為該類型值的隱式方法。

例如,以下程式碼定義從 IntLong 的隱式轉換

import scala.language.implicitConversions

implicit def int2long(x: Int): Long = x.toLong

這是一個可轉換為類型 Int => Long 值的隱式方法。

請參閱下方「當心隱式轉換的力量」一節,了解開頭的子句 import scala.language.implicitConversions 的說明。

在 Scala 3 中,從類型 S 到類型 T 的隱式轉換是由 given 執行個體定義的,類型為 scala.Conversion[S, T]。為了與 Scala 2 相容,也可以透過隱式方法定義(在 Scala 2 標籤中閱讀更多資訊)。

例如,此程式碼定義從 IntLong 的隱式轉換

given int2long: Conversion[Int, Long] with
  def apply(x: Int): Long = x.toLong

與其他 given 定義一樣,隱式轉換可以是匿名的

given Conversion[Int, Long] with
  def apply(x: Int): Long = x.toLong

使用別名,可以更簡潔地表達為

given Conversion[Int, Long] = (x: Int) => x.toLong

使用隱式轉換

隱式轉換應用於兩種情況

  1. 如果表達式 e 的類型為 S,且 S 不符合表達式的預期類型 T
  2. 在選擇 e.m 中,如果 e 的類型為 S,且選擇器 m 沒有表示 S 的成員(支援 Scala-2 風格 擴充方法)。

在第一種情況下,尋找適用於 e 且其結果類型符合 T 的轉換 c

在上述範例中,當我們將類型為 Int 的引數 id 傳遞給方法 findUserById 時,會插入隱式轉換 int2long(id)

在第二種情況下,尋找適用於 e 且其結果包含名為 m 的成員的轉換 c

一個範例是比較兩個字串 "foo" < "bar"。在這種情況下,String 沒有成員 <,因此會插入隱式轉換 Predef.augmentString("foo") < "bar"。(scala.Predef 會自動匯入所有 Scala 程式。)

如何將隱式轉換納入範圍?

當編譯器尋找適用的轉換時

  • 首先,它會查看目前的詞彙範圍
    • 在目前範圍或外部範圍中定義的隱式轉換
    • 匯入的隱式轉換
    • 由萬用字元匯入的隱式轉換(僅限 Scala 2)
  • 然後,它會查看與引數類型 S 或預期類型 T 關聯伴隨物件。與類型 X 關聯的伴隨物件為
    • 伴隨物件 X 本身
    • X 的任何繼承類型關聯的伴隨物件
    • X 中的任何類型引數關聯的伴隨物件
    • 如果 X 是內部類別,則它會嵌入其中外部物件

例如,考慮在物件 Conversions 中定義的隱式轉換 fromStringToUser

import scala.language.implicitConversions

object Conversions {
  implicit def fromStringToUser(name: String): User = (name: String) => User(name)
}
object Conversions:
  given fromStringToUser: Conversion[String, User] = (name: String) => User(name)

以下匯入將等效地將轉換納入範圍

import Conversions.fromStringToUser
// or
import Conversions._
import Conversions.fromStringToUser
// or
import Conversions.given
// or
import Conversions.{given Conversion[String, User]}

請注意,在 Scala 3 中,萬用字元匯入 (即 import Conversions.*) 不會匯入指定的定義。

在引言範例中,從 IntLong 的轉換不需要匯入,因為它是在物件 Int 中定義,而該物件是類型 Int 的伴侶物件。

進一步閱讀:Scala 在哪裡尋找隱式內容? (在 Stackoverflow 上)

小心隱式轉換的力量

由於隱式轉換在不加選擇地使用時可能會造成陷阱,因此編譯器在編譯隱式轉換定義時會發出警告。

若要關閉警告,請執行下列任一動作

  • scala.language.implicitConversions 匯入隱式轉換定義的範圍
  • 使用 -language:implicitConversions 呼叫編譯器

當編譯器套用轉換時,不會發出警告。

由於隱式轉換在不加選擇地使用時可能會造成陷阱,因此編譯器在兩種情況下會發出警告

  • 編譯 Scala 2 風格的隱式轉換定義時。
  • 在呼叫位置,其中給定的 scala.Conversion 執行個體會插入為轉換。

若要關閉警告,請執行下列任一動作

  • scala.language.implicitConversions 匯入下列範圍
    • Scala 2 風格的隱式轉換定義
    • 呼叫位置,其中給定的 scala.Conversion 執行個體會插入為轉換。
  • 使用 -language:implicitConversions 呼叫編譯器

此頁面的貢獻者