在 GitHub 上編輯此頁面

程式化結構類型

動機

某些使用案例(例如資料庫存取建模)在靜態類型語言中比在動態類型語言中更為尷尬:使用動態類型語言,將列建模為記錄或物件非常自然,並使用簡單的點號表示法(例如 row.columnName)選取項目。

在靜態類型語言中獲得相同的體驗需要為資料庫處理產生的每一列定義一個類別(包括聯結和投影產生的列),並建立一個在列和代表它的類別之間進行對應的架構。

這需要大量的樣板程式碼,這導致開發人員為了更簡單的架構(其中欄位名稱表示為字串並傳遞給其他運算子(例如 row.select("columnName")))而放棄靜態型別的優點。這種方法放棄了靜態型別的優點,而且仍然不如動態型別版本自然。

結構類型有助於在我們希望在動態環境中支援簡單的點號表示法而不失去靜態型別優點的情況。它們允許開發人員使用點號表示法並設定如何解析欄位和方法。

範例

以下是結構類型 Person 的範例

type Person = Record { val name: String; val age: Int }

類型 Person 為其父類型 Record 新增一個精緻化,定義兩個欄位 nameage。我們稱此精緻化為結構性的,因為 nameage 並未定義在父類型中。但它們仍存在於類型 Person 的成員中。

這讓我們可以在編譯時檢查存取是否有效

val person: Person = ???
println(s"${person.name} is ${person.age} years old.") // works
println(person.email) // error: value email is not a member of Person

Record 如何定義,以及 person.name 如何解析?

Record 是一個類別,它擴充標記特質 scala.Selectable,並定義一個方法 selectDynamic,將欄位名稱對應到其值。選取結構類型的一個成員,在語法上等同於呼叫此方法。Scala 編譯器將選取 person.nameperson.age 轉譯為

person.selectDynamic("name").asInstanceOf[String]
person.selectDynamic("age").asInstanceOf[Int]

例如,Record 可以定義如下

class Record(elems: (String, Any)*) extends Selectable:
  private val fields = elems.toMap
  def selectDynamic(name: String): Any = fields(name)

這讓我們可以建立 Person 的實例,如下所示

val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]

在此範例中,父類型 Record 是個泛型類別,可以用其 elems 參數表示任意的記錄。此參數是一個序列,包含標籤(類型為 String)和值(類型為 Any)的配對。當我們建立一個 Person 作為 Record 時,我們必須透過類型轉換來斷言該記錄定義了正確類型的正確欄位。Record 本身類型太過薄弱,因此編譯器無法在沒有使用者協助的情況下得知這一點。在實務上,結構類型和其底層泛型表示之間的關聯,很可能由資料庫層來處理,因此不會是最終使用者的問題。

除了 selectDynamic 之外,Selectable 類別有時也會定義一個方法 applyDynamic。這可以用來轉譯結構成員的函式呼叫。因此,如果 aSelectable 的實例,結構呼叫 a.f(b, c) 將轉譯為

a.applyDynamic("f")(b, c)

使用 Java 反射

使用 SelectableJava 反射,我們可以從不相關的類別中選擇成員。

在使用 Java 反射進行結構性呼叫之前,應該考慮其他選項。例如,有時可以使用 類型類別 來獲得更具模組化有效率的架構。

例如,我們希望同時為 FileInputStreamChannel 類別提供行為,方法是呼叫它們的 close 方法,然而,這些類別是不相關的,也就是說沒有具有 close 方法的共同超類型。因此,以下我們定義一個結構類型 Closeable,它定義一個 close 方法。

type Closeable = { def close(): Unit }

class FileInputStream:
  def close(): Unit

class Channel:
  def close(): Unit

理想情況下,我們會將一個共同介面新增到這兩個類別中,以定義 close 方法,但它們是在我們無法控制的函式庫中定義的。作為折衷方案,我們可以使用結構類型為 autoClose 方法定義單一實作

import scala.reflect.Selectable.reflectiveSelectable

def autoClose(f: Closeable)(op: Closeable => Unit): Unit =
  try op(f) finally f.close()

呼叫 f.close() 需要 Closeable 延伸 Selectable,以識別並在接收器 f 中呼叫 close 方法。根據 Java 反射,透過匯入上面顯示的 reflectiveSelectable,可以啟用對 Selectable 的通用隱式轉換。然後,「幕後」發生的事情如下

  • 隱式轉換會將 f 包裝在 scala.reflect.Selectable 的實例中(它是 Selectable 的子類型)。

  • 然後,編譯器會將包裝的 f 上的 close 呼叫轉換為 applyDynamic 呼叫。最終結果是

    reflectiveSelectable(f).applyDynamic("close")()
    
  • reflectiveSelectable 的結果中 applyDynamic 的實作使用 Java 反射 在執行階段尋找並呼叫值 f 中具有零個參數的方法 close

像這樣的結構呼叫往往比一般方法呼叫慢很多。強制匯入 reflectiveSelectable 作為一個標示,表示有低效率的事情正在發生。

注意:在 Scala 2 中,Java 反射是結構類型中唯一可用的機制,且會自動啟用,不需要 reflectiveSelectable 轉換。然而,為了警告低效率的派送,Scala 2 需要語言匯入 import scala.language.reflectiveCalls

可擴充性

可以定義 Selectable 的新實例,以支援 Java 反射以外的存取方式,這將啟用本文件開頭給出的資料庫存取範例等用法。

本機 Selectable 實例

延伸 Selectable 的本機和匿名類別會比其他類別獲得更精緻的類型。以下是範例

trait Vehicle extends reflect.Selectable:
  val wheels: Int

val i3 = new Vehicle: // i3: Vehicle { val range: Int }
  val wheels = 4
  val range = 240

i3.range

此範例中 i3 的類型為 Vehicle { val range: Int }。因此,i3.range 是合法的。由於基底類別 Vehicle 沒有定義 range 欄位或方法,因此我們需要結構派送才能存取初始化 id3 的匿名類別的 range 欄位。結構派送是由 Vehicle 的基底特徵 reflect.Selectable 實作,它定義必要的 selectDynamic 成員。

Vehicle 也可以擴充其他一些 scala.Selectable 子類別,這些子類別以不同的方式實作 selectDynamicapplyDynamic。但如果它根本沒有擴充 Selectable,程式碼就不再會進行類型檢查

trait Vehicle:
  val wheels: Int

val i3 = new Vehicle: // i3: Vehicle
  val wheels = 4
  val range = 240

i3.range // error: range is not a member of `Vehicle`

不同之處在於,不擴充 Selectable 的匿名類別類型僅由類別的父類型組成,而不會新增任何細化。因此,i3 現在只有 Vehicle 類型,而選取 i3.range 會產生「找不到成員」錯誤。

請注意,在 Scala 2 中,所有區域和匿名類別都可以產生具有細化類型的值。但只能使用語言匯入 reflectiveCalls 選取由這些細化所定義的成員。

scala.Dynamic 的關係

這裡顯然有一些與 scala.Dynamic 的關聯,因為兩者都以程式方式選取成員。但也有部分差異。

  • 完全動態選取並非類型安全,但結構選取是,只要結構類型與基礎值的對應關係如所述。

  • 兩個存取操作 selectDynamicapplyDynamic 由兩種方法共用。在 Selectable 中,applyDynamic 也可能採用 java.lang.Class 引數,表示方法的正式參數類型。

  • updateDynamicDynamic 獨有的,但如前所述,這個事實可能會改變,不應視為假設。

更多詳細資訊