在 GitHub 上編輯此頁面

程式結構類型 - 更多詳細資訊

語法

SimpleType    ::= ... | Refinement
Refinement    ::= ‘{’ RefineStatSeq ‘}’
RefineStatSeq ::=  RefineStat {semi RefineStat}
RefineStat    ::= ‘val’ VarDcl | ‘def’ DefDcl | ‘type’ {nl} TypeDcl

結構型別的實作

標準函式庫定義一個通用標記特徵 scala.Selectable

trait Selectable extends Any

仰賴 Java 反射Selectable 實作可在標準函式庫中取得:scala.reflect.Selectable。對於無法使用 Java 反射的平台,可以構想其他實作。

Selectable 的實作必須提供 selectDynamicapplyDynamic 方法,或兩者都提供。這些方法可以是 Selectable 實作的成員,也可以是擴充方法。

selectDynamic 方法會取得欄位名稱,並傳回與該名稱相關聯的 Selectable 值。其簽章應採用以下形式

def selectDynamic(name: String): T

通常,回傳型別 TAny

scala.Dynamic 不同,updateDynamic 方法沒有特殊意義。不過,我們保留在未來賦予其意義的權利。因此,建議不要在 Selectable 中定義任何稱為 updateDynamic 的成員。

applyDynamic 方法用於套用至引數的選取。它會取得方法名稱,以及可能表示其參數型別的 Class,以及要傳遞給函數的引數。其簽章應採用以下兩個形式之一

def applyDynamic(name: String)(args: Any*): T
def applyDynamic(name: String, ctags: Class[?]*)(args: Any*): T

兩個版本都將實際參數傳遞給 args 參數。第二個版本除了會接收 java.lang.Classes 的 vararg 參數外,還會識別方法的參數類別。如果 applyDynamic 是使用 Java 反射實作的,則需要此參數,但它在其他情況下也可能很有用。selectDynamicapplyDynamic 也可以在使用子句中採用其他內容參數。這些參數會在呼叫位置以正常方式解析。

給定類型為 C { Rs } 的值 v,其中 C 是類別參考,而 Rs 是結構性精簡宣告,且給定類型為 Uv.a,我們會考慮三種不同的情況

  • 如果 U 是值類型,我們會將 v.a 對應到

    v.selectDynamic("a").asInstanceOf[U]
    
  • 如果 U 是方法類型 (T11, ..., T1n)...(TN1, ..., TNn): R 且它不是相依方法類型,我們會將 v.a(a11, ..., a1n)...(aN1, ..., aNn) 對應到

    v.applyDynamic("a")(a11, ..., a1n, ..., aN1, ..., aNn)
      .asInstanceOf[R]
    

    如果此呼叫解析為採用 Class[?]* 參數的第二個形式的 applyDynamic 方法,我們會進一步將此呼叫改寫為

    v.applyDynamic("a", c11, ..., c1n, ..., cN1, ... cNn)(
      a11, ..., a1n, ..., aN1, ..., aNn)
      .asInstanceOf[R]
    

    其中每個 c_ij 都是形式參數 Tij 類型的文字 java.lang.Class[?],亦即 classOf[Tij]

  • 如果 U 既不是值類型,也不是方法類型或相依方法類型,則會發出錯誤。

請注意,v 的靜態類型不一定必須符合 Selectable,也不需要將 selectDynamicapplyDynamic 作為成員。只要有一個隱式轉換可以將 v 轉換為 Selectable 就足夠了,而且選擇方法也可以作為 擴充方法 使用。

結構類型限制

  • 無法透過結構呼叫來呼叫相依方法。

  • 精緻化可能不會引入重載:如果精緻化指定方法 m 的簽章,而且 m 也在精緻化的父類型中定義,則新簽章必須正確覆寫現有簽章。

  • 結構精緻化的子類型化必須保留已清除的參數類型:假設我們要證明 S <: T { def m(x: A): B }。然後,S 必須有一個成員方法 m,可以接受類型為 A 的引數。此外,如果 m 不是 T 的成員(即精緻化是結構性的),則會套用額外條件。在這種情況下,S 的成員定義 m 將有一個類型為 A' 的參數。額外條件是 A'A 的清除相同。以下是範例

    class Sink[A] { def put(x: A): Unit = {} }
    val a = Sink[String]()
    val b: { def put(x: String): Unit } = a  // error
    b.put("abc") // looks for a method with a `String` parameter
    

    倒數第二行沒有類型正確,因為類別 Sinkput 的參數類型的清除為 Object,但是 b 類型的 put 參數的清除為 String。這個額外條件是必要的,因為我們必須使用某種(尚未知道的)反射形式來呼叫 b 類型中 put 等結構成員。此條件可確保精緻化的靜態已知參數類型對應到執行時期所選呼叫目標的參數類型(清除後)。

    大多數反射調度演算法需要知道確切的清除參數類型。例如,如果上面的範例會進行類型檢查,則最後一行中的呼叫 b.put("abc") 會在 b 的執行時期類型中尋找一個 put 方法,該方法會接受一個 String 參數。但是 put 方法是來自類別 Sink 的方法,該方法會接受一個 Object 參數。因此,呼叫會在執行時期失敗,並出現 NoSuchMethodException

    我們可能會希望有一個「更智慧」的反射式調度演算法,它不需要精確的參數類型比對。不幸的是,只要有重載的可能性,這總是會遇到歧義。例如,繼續上面的範例,我們可能會引入一個新的 Sink 子類別 Sink1,並變更 a 的定義如下

    class Sink1[A] extends Sink[A] { def put(x: "123") = ??? }
    val a: Sink[String] = Sink1[String]()
    

    現在在 b 的執行時期類型中有兩個 put 方法,分別以擦除的參數類型 ObjectString。然而,動態調度仍然需要轉到第一個 put 方法,即使第二個看起來更匹配。

    對於我們實際上可以在不知道精確參數類型的情況下實作反射的案例(例如,如果靜態重載被動態調度的多重方法取代),有一個逃生艙口。對於延伸 scala.Selectable.WithoutPreciseParameterTypes 的類型,會省略簽章檢查。範例

    trait MultiMethodSelectable extends Selectable.WithoutPreciseParameterTypes:
      // Assume this version of `applyDynamic` can be implemented without knowing
      // precise parameter types `paramTypes`:
      def applyDynamic(name: String, paramTypes: Class[_]*)(args: Any*): Any = ???
    
    class Sink[A] extends MultiMethodSelectable:
      def put(x: A): Unit = {}
    
    val a = new Sink[String]
    val b: MultiMethodSelectable { def put(x: String): Unit } = a  // OK
    

與 Scala 2 結構化類型的差異

  • Scala 2 透過 Java 反射支援結構化類型。與 Scala 3 不同,結構化呼叫不依賴於 Selectable 等機制,而且無法避免反射。
  • 在 Scala 2 中,精煉可以引入重載。
  • 在 Scala 2 中,精煉允許變動的 var。在 Scala 3 中,不再允許。
  • Scala 2 沒有對結構化類型的子類型化施加「相同擦除」限制。它允許一些呼叫在執行時期失敗。

背景

如需更多資訊,請參閱 重新思考結構化類型