程式化結構類型
動機
某些使用案例(例如資料庫存取建模)在靜態類型語言中比在動態類型語言中更為尷尬:使用動態類型語言,將列建模為記錄或物件非常自然,並使用簡單的點號表示法(例如 row.columnName
)選取項目。
在靜態類型語言中獲得相同的體驗需要為資料庫處理產生的每一列定義一個類別(包括聯結和投影產生的列),並建立一個在列和代表它的類別之間進行對應的架構。
這需要大量的樣板程式碼,這導致開發人員為了更簡單的架構(其中欄位名稱表示為字串並傳遞給其他運算子(例如 row.select("columnName")
))而放棄靜態型別的優點。這種方法放棄了靜態型別的優點,而且仍然不如動態型別版本自然。
結構類型有助於在我們希望在動態環境中支援簡單的點號表示法而不失去靜態型別優點的情況。它們允許開發人員使用點號表示法並設定如何解析欄位和方法。
範例
以下是結構類型 Person
的範例
type Person = Record { val name: String; val age: Int }
類型 Person
為其父類型 Record
新增一個精緻化,定義兩個欄位 name
和 age
。我們稱此精緻化為結構性的,因為 name
和 age
並未定義在父類型中。但它們仍存在於類型 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.name
和 person.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
。這可以用來轉譯結構成員的函式呼叫。因此,如果 a
是 Selectable
的實例,結構呼叫 a.f(b, c)
將轉譯為
a.applyDynamic("f")(b, c)
使用 Java 反射
使用 Selectable
和 Java 反射,我們可以從不相關的類別中選擇成員。
在使用 Java 反射進行結構性呼叫之前,應該考慮其他選項。例如,有時可以使用 類型類別 來獲得更具模組化且有效率的架構。
例如,我們希望同時為 FileInputStream
和 Channel
類別提供行為,方法是呼叫它們的 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
子類別,這些子類別以不同的方式實作 selectDynamic
和 applyDynamic
。但如果它根本沒有擴充 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
的關聯,因為兩者都以程式方式選取成員。但也有部分差異。
-
完全動態選取並非類型安全,但結構選取是,只要結構類型與基礎值的對應關係如所述。
-
兩個存取操作
selectDynamic
和applyDynamic
由兩種方法共用。在Selectable
中,applyDynamic
也可能採用java.lang.Class
引數,表示方法的正式參數類型。 -
updateDynamic
是Dynamic
獨有的,但如前所述,這個事實可能會改變,不應視為假設。