在 GitHub 上編輯此頁面

匯出子句

匯出子句定義物件中所選成員的別名。範例

class BitMap
class InkJet

class Printer:
  type PrinterType
  def print(bits: BitMap): Unit = ???
  def status: List[String] = ???

class Scanner:
  def scan(): BitMap = ???
  def status: List[String] = ???

class Copier:
  private val printUnit = new Printer { type PrinterType = InkJet }
  private val scanUnit = new Scanner

  export scanUnit.scan
  export printUnit.{status as _, *}

  def status: List[String] = printUnit.status ++ scanUnit.status

兩個 export 子句在類別 Copier 中定義下列 匯出別名

final def scan(): BitMap            = scanUnit.scan()
final def print(bits: BitMap): Unit = printUnit.print(bits)
final type PrinterType              = printUnit.PrinterType

它們可以在 Copier 內部以及外部存取

val copier = new Copier
copier.print(copier.scan())

export 子句的格式與匯入子句相同。其一般形式為

export path . { sel_1, ..., sel_n }

它包含限定詞運算式 path(必須是穩定的識別碼),後面接著一個或多個選擇器 sel_i,用於識別哪些項目會取得別名。選擇器可以是下列其中一種形式

  • 簡單選擇器 x 會為 path 中所有符合資格且名稱為 x 的成員建立別名。
  • 重新命名選擇器 x as y 會為 path 中所有符合資格且名稱為 x 的成員建立別名,但別名名稱為 y,而非 x
  • 省略選擇器 x as _ 可防止 x 被後續萬用字元選擇器指定別名。
  • 指定選擇器 given x 具有選用型別繫結 x。它會為所有符合 x 的符合資格指定實例建立別名,如果省略 x 則為 Any,但先前簡單、重新命名或省略選擇器所命名的成員除外。
  • 萬用字元選擇器 * 會為 path 的所有符合資格成員建立別名,但指定實例、編譯器產生的合成成員和先前簡單、重新命名或省略選擇器所命名的成員除外。
    注意事項
    • 符合資格的建構函式代理程式也會包含在內,即使它們是合成成員。
    • 由匯出建立的成員也會包含在內。它們是由編譯器建立的,但不會被視為合成。

如果符合下列所有條件,則成員符合資格

  • 它的擁有者不是包含匯出子句的類別(*)的基本類別,
  • 成員不會覆寫具體定義,而該定義的擁有者為包含匯出子句的類別的基本類別。
  • 它在匯出子句中可存取,
  • 它不是建構函式,也不是物件的(合成)類別部分,
  • 它是一個指定實例(使用 given 宣告),當且僅當匯出來自指定選擇器

如果簡單或重新命名選擇器未識別任何符合資格的成員,則為編譯時期錯誤。

型別成員由型別定義指定別名,而術語成員由方法定義指定別名。例如

object O:
  class C(val x: Int)
  def m(c: C): Int = c.x + 1
export O.*
  // generates
  //   type C = O.C
  //   def m(c: O.C): Int = O.m(c)

匯出別名會複製它們所參考成員的型別和值參數。匯出別名永遠是 final。指定實例的別名會再次定義為指定(舊式暗示的別名為 implicit)。延伸函式的別名會再次定義為延伸函式。內聯方法或值的別名會再次定義為 inline。別名沒有其他可用的修飾詞。這對覆寫有下列影響

  • 匯出別名無法被覆寫,因為它們是最終的。
  • 匯出別名無法覆寫基本類別中的具體成員,因為它們未標記為 override
  • 不過,匯出別名可以實作基本類別的遞延成員。

對於公開值定義的匯出別名,如果在限定符號路徑中存取時未參考私人值,編譯器會將它們標記為「穩定」,而且它們的結果型別是別名定義的單例型別。這表示它們可用作穩定識別符號路徑的一部分,即使它們在技術上是方法。例如,下列情況是允許的

class C { type T }
object O { val c: C = ... }
export O.c
def f: c.T = ...

限制

  1. 匯出子句可以出現在類別中,也可以出現在頂層。匯出子句無法出現在區塊中作為陳述式。

  2. 如果匯出子句包含萬用字元或指定選擇器,則禁止其限定符號路徑參考套件。這是因為目前還不知道如何安全追蹤萬用字元相依性,以進行增量編譯。

  3. 匯出重新命名會隱藏與目標名稱相符的未重新命名的匯出。例如,由於 B 已被 A as B 重新命名隱藏,因此下列子句會無效。

    export {A as B, B}        // error: B is hidden
    
  4. 匯出子句中的重新命名必須具有成對不同的目標名稱。例如,下列子句會無效

    export {A as C, B as C}   // error: duplicate renaming
    
  5. 像這樣的簡單重新命名匯出

    export status as stat
    

    尚未受到支援。它們會違反匯出的 a 無法已是包含匯出的物件成員的限制。此限制可能會在未來解除。

(*) 注意:除非另有說明,否則本討論中的「類別」一詞也包含物件和特質定義。

動機

優先採用組合而非繼承是一項標準建議。這實際上是運用最小權限原則:組合將元件視為黑盒子,而繼承則可透過覆寫影響元件的內部運作。有時繼承所暗示的緊密結合是解決問題的最佳方案,但若非必要,較鬆散的組合結合會更好。

到目前為止,包括 Scala 在內的物件導向語言讓使用繼承比組合容易得多。繼承只需要 extends 子句,而組合則需要詳細說明轉發順序。因此,在這個意義上,物件導向語言會推動程式設計人員採用通常過於強大的解決方案。匯出子句可糾正這種不平衡。它們讓組合關係像繼承關係一樣簡潔且易於表達。匯出子句也比延伸子句提供更大的彈性,因為成員可以重新命名或省略。

匯出子句也填補了從套件物件轉移到頂層定義所造成的空白。在這種轉移中偶爾會遺失一個有用的慣用語,即套件物件繼承自某個類別。此慣用語通常用於類似外觀的模式中,以讓內部組合的成員可供套件使用者使用。頂層定義並未包裝在使用者定義的物件中,因此它們無法繼承任何東西。然而,頂層定義可以是匯出子句,這以更安全且更靈活的方式支援外觀設計模式。

擴充中的匯出子句

匯出子句也可能出現在擴充中。

範例

class StringOps(x: String):
  def *(n: Int): String = ...
  def capitalize: String = ...

extension (x: String)
  def take(n: Int): String = x.substring(0, n)
  def drop(n: Int): String = x.substring(n)
  private def moreOps = new StringOps(x)
  export moreOps.*

在這種情況下,限定詞表達式必須是識別碼,用於參照同一擴充子子句中唯一的無參數擴充子方法。匯出會為限定詞路徑結果中的所有可存取術語成員建立擴充子方法。例如,上述擴充子會擴充為

extension (x: String)
  def take(n: Int): String = x.substring(0, n)
  def drop(n: Int): String = x.substring(n)
  private def moreOps = StringOps(x)
  def *(n: Int): String = moreOps.*(n)
  def capitalize: String = moreOps.capitalize

語法變更

TemplateStat      ::=  ...
                    |  Export
TopStat           ::=  ...
                    |  Export
ExtMethod         ::=  ...
                    |  Export
Export            ::=  ‘export’ ImportExpr {‘,’ ImportExpr}
ImportExpr        ::=  SimpleRef {‘.’ id} ‘.’ ImportSpec
ImportSpec        ::=  NamedSelector
                    |  WildcardSelector
                    | ‘{’ ImportSelectors) ‘}’
NamedSelector     ::=  id [‘as’ (id | ‘_’)]
WildCardSelector  ::=  ‘*’ | ‘given’ [InfixType]
ImportSelectors   ::=  NamedSelector [‘,’ ImportSelectors]
                    |  WildCardSelector {‘,’ WildCardSelector}

匯出子句的詳細說明

匯出子句會引發有關類型檢查期間詳細說明順序的問題。請考慮下列範例

class B { val c: Int }
object A { val b = new B }
export A.*
export b.*

export b.* 子句是否合法?如果是,它會匯出什麼?是否等於 export A.b.*?如果我們交換最後兩個子句,會如何?

export b.*
export A.*

為避免此類棘手的問題,我們修正匯出的詳細說明順序,如下所示。

當封閉物件或類型的類型資訊完成時,會處理匯出子句。截至目前為止,完成包含下列步驟

  1. 詳細說明類型的所有註解。

  2. 詳細說明類型的參數。

  3. 詳細說明類型的自我類型(如果已提供)。

  4. 將類型的所有定義輸入為類型成員,並依需求完成類型。

  5. 確定類型的所有父類型的類型。

    使用匯出子句,會新增下列步驟

  6. 計算匯出子句中所有路徑的類型。

  7. 輸入匯出子句中所有路徑的合格成員的匯出別名。

步驟 6 和 7 按順序完成非常重要:我們首先計算匯出子句中所有路徑的類型,然後才會將任何匯出別名輸入為類型成員。這表示匯出子句的路徑無法參照同一個類型的另一個匯出子句提供的別名。