Scala 3 — 書籍

封裝與匯入

語言

Scala 使用封裝建立命名空間,讓您可以將程式模組化,並協助避免命名空間衝突。Scala 支援 Java 使用的封裝命名樣式,也支援 C++ 和 C# 等語言使用的「大括號」命名空間表示法。

Scala 匯入成員的方法也類似於 Java,但更靈活。使用 Scala,您可以

  • 匯入封裝、類別、物件、特質和方法
  • 將匯入陳述式放在任何地方
  • 在匯入時隱藏和重新命名成員

下列範例示範這些功能。

建立封裝

封裝是透過在 Scala 檔案的頂端宣告一個或多個封裝名稱建立的。例如,當您的網域名稱是 acme.com,而且您正在應用程式 myappmodel 封裝中工作時,您的封裝宣告會如下所示

package com.acme.myapp.model

class Person ...

根據慣例,封裝名稱應全部使用小寫,正式的命名慣例為 <top-level-domain>.<domain-name>.<project-name>.<module-name>

儘管並非必要,但套件名稱通常遵循目錄結構名稱,因此如果您遵循此慣例,此專案中的 Person 類別會出現在 MyApp/src/main/scala/com/acme/myapp/model/Person.scala 檔案中。

在同一個檔案中使用多個套件

上述語法套用於整個原始碼檔案:檔案 Person.scala 中的所有定義都屬於套件 com.acme.myapp.model,根據檔案開頭的套件條款。

或者,可以撰寫僅套用於其包含的定義的套件條款

package users {

  package administrators {  // the full name of this package is users.administrators
    class AdminUser        // the full name of this class users.administrators.AdminUser
  }
  package normalusers {     // the full name of this package is users.normalusers
    class NormalUser       // the full name of this class is users.normalusers.NormalUser
  }
}
package users:

  package administrators:  // the full name of this package is users.administrators
    class AdminUser        // the full name of this class is users.administrators.AdminUser

  package normalusers:     // the full name of this package is users.normalusers
    class NormalUser       // the full name of this class is users.normalusers.NormalUser

請注意,套件名稱後接冒號,且套件內的定義會縮排。

此方法的優點在於它允許套件巢狀,並提供對範圍和封裝的更明顯控制,特別是在同一個檔案中。

匯入陳述,第 1 部分

匯入陳述用於存取其他套件中的實體。匯入陳述分為兩大類

  • 匯入類別、特質、物件、函式和方法
  • 匯入 given 條款

如果您習慣使用 Java 等語言,第一類的匯入陳述類似於 Java 使用的陳述,語法略有不同,允許更大的彈性。以下範例說明了其中一些彈性

import users._                            // import everything from the `users` package
import users.User                         // import only the `User` class
import users.{User, UserPreferences}      // import only two selected members
import users.{UserPreferences => UPrefs}  // rename a member as you import it
import users.*                            // import everything from the `users` package
import users.User                         // import only the `User` class
import users.{User, UserPreferences}      // import only two selected members
import users.{UserPreferences as UPrefs}  // rename a member as you import it

這些範例旨在讓您了解第一類 import 陳述如何運作。它們在以下小節中會做更詳細的說明。

匯入陳述也用於將 given 執行個體匯入範圍。這些會在本章節的最後討論。

在繼續之前的一點說明

存取同一個套件的成員不需要匯入條款。

匯入一個或多個成員

在 Scala 中,您可以像這樣從套件匯入一個成員

import scala.concurrent.Future

並像這樣匯入多個成員

import scala.concurrent.Future
import scala.concurrent.Promise
import scala.concurrent.blocking

在匯入多個成員時,您可以像這樣更簡潔地匯入它們

import scala.concurrent.{Future, Promise, blocking}

當您想要從 scala.concurrent 套件匯入所有內容時,請使用此語法

import scala.concurrent._
import scala.concurrent.*

在匯入時重新命名成員

有時,在匯入實體時重新命名它們有助於避免名稱衝突。例如,如果您想要同時使用 Scala List 類別和 java.util.List 類別,您可以在匯入時重新命名 java.util.List 類別

import java.util.{List => JavaList}
import java.util.{List as JavaList}

現在,您使用名稱 JavaList 來參考該類別,並使用 List 來參考 Scala 清單類別。

您也可以使用此語法一次重新命名多個成員

import java.util.{Date => JDate, HashMap => JHashMap, _}
import java.util.{Date as JDate, HashMap as JHashMap, *}

那行程式碼表示:「重新命名 DateHashMap 類別,如所示,並匯入 java.util 套件中的所有其他內容,而不重新命名任何其他成員。」

在匯入時隱藏成員

您也可以在匯入過程中隱藏成員。此 import 陳述式隱藏了 java.util.Random 類別,同時匯入 java.util 套件中的所有其他內容

import java.util.{Random => _, _}
import java.util.{Random as _, *}

如果您嘗試存取 Random 類別,它將無法運作,但您可以存取該套件中的所有其他成員

val r = new Random   // won’t compile
new ArrayList        // works

隱藏多個成員

要在匯入過程中隱藏多個成員,請在使用最後的萬用字元匯入之前列出它們

import java.util.{List => _, Map => _, Set => _, _}
scala> import java.util.{List as _, Map as _, Set as _, *}

這些類別再次被隱藏,但您可以在 java.util 中使用所有其他類別

scala> new ArrayList[String]
val res0: java.util.ArrayList[String] = []

由於這些 Java 類別被隱藏,因此您也可以使用 Scala ListSetMap 類別,而不會發生命名衝突

scala> val a = List(1, 2, 3)
val a: List[Int] = List(1, 2, 3)

scala> val b = Set(1, 2, 3)
val b: Set[Int] = Set(1, 2, 3)

scala> val c = Map(1 -> 1, 2 -> 2)
val c: Map[Int, Int] = Map(1 -> 1, 2 -> 2)

在任何地方使用匯入

在 Scala 中,import 陳述式可以在任何地方。它們可以用於原始碼檔案的頂端

package foo

import scala.util.Random

class ClassA {
  def printRandom(): Unit = {
    val r = new Random   // use the imported class
    // more code here...
  }
}
package foo

import scala.util.Random

class ClassA:
  def printRandom(): Unit =
    val r = new Random   // use the imported class
    // more code here...

如果您喜歡,您也可以在需要它們的地方附近使用 import 陳述式

package foo

class ClassA {
  import scala.util.Random   // inside ClassA
  def printRandom(): Unit = {
    val r = new Random
    // more code here...
  }
}

class ClassB {
  // the Random class is not visible here
  val r = new Random   // this code will not compile
}
package foo

class ClassA:
  import scala.util.Random   // inside ClassA
  def printRandom(): Unit =
    val r = new Random
    // more code here...

class ClassB:
  // the Random class is not visible here
  val r = new Random   // this code will not compile

「靜態」匯入

當您想要以類似於 Java「靜態匯入」方法的方式匯入成員時,您可以直接參照成員名稱,而不需要加上它們的類別名稱,請使用以下方法。

使用此語法匯入 Java Math 類別的所有靜態成員

import java.lang.Math._
import java.lang.Math.*

現在,您可以存取靜態 Math 類別方法,例如 sincos,而不需要在它們前面加上類別名稱

import java.lang.Math._

val a = sin(0)    // 0.0
val b = cos(PI)   // -1.0
import java.lang.Math.*

val a = sin(0)    // 0.0
val b = cos(PI)   // -1.0

預設匯入的套件

兩個套件會隱含匯入到您所有原始碼檔案的範圍中

  • java.lang.*
  • scala.*

Scala 物件 Predef 的成員也會預設匯入。

如果您曾經疑惑過為什麼可以使用 ListVectorMap 等類別,而不用匯入它們,這是因為 Predef 物件中的定義。

處理命名衝突

在罕見的命名衝突事件中,您需要從專案根目錄匯入某些內容,請在套件名稱前面加上 _root_

package accounts

import _root_.accounts._
package accounts

import _root_.accounts.*

匯入 given 實例

正如您在 Contextual Abstractions 章節中看到的,在 Scala 3 中,import 陳述式的特殊形式用於匯入 given 實例。基本形式顯示在這個範例中

object A:
  class TC
  given tc: TC
  def f(using TC) = ???

object B:
  import A.*       // import all non-given members
  import A.given   // import the given instance

在此程式碼中,物件 Bimport A.* 子句會匯入 A 的所有成員,除了 given 實例 tc。相反地,第二個匯入 import A.given 會匯入那個 given 實例。兩個 import 子句也可以合併成一個

object B:
  import A.{given, *}

在 Scala 2 中,這種匯入樣式不存在。隱含定義總是透過萬用字元匯入。

討論

萬用字元選擇器 * 會將除 given 或擴充功能以外的所有定義納入範圍,而 given 選擇器會將所有 givens(包括由擴充功能產生的)納入範圍。

這些規則有兩個主要好處

  • 範圍內的 givens 來自何處會更清楚。特別是,不可能在其他萬用字元匯入的長清單中隱藏匯入的 givens。
  • 它可以在不匯入其他任何內容的情況下匯入所有 givens。這特別重要,因為 givens 可以是匿名的,因此通常使用命名匯入並不實際。

依類型匯入

由於 givens 可以是匿名的,因此透過它們的名稱匯入它們並不總是實際可行,而萬用字元匯入通常用於代替。依類型匯入 提供了比萬用字元匯入更具體的替代方案,這使得匯入的內容更清楚

import A.{given TC}

這會匯入任何在 A 中的 given,其類型符合 TC。匯入多種類型 T1,...,Tn 的 givens,會以多個 given 選擇器表示

import A.{given T1, ..., given Tn}

匯入參數化類型的所有 given 執行個體,會以萬用字元引數表示。例如,當您有這個 object

object Instances:
  given intOrd: Ordering[Int]
  given listOrd[T: Ordering]: Ordering[List[T]]
  given ec: ExecutionContext = ...
  given im: Monoid[Int]

這個匯入陳述匯入了 intOrdlistOrdec 執行個體,但遺漏了 im 執行個體,因為它不符合任何指定的界線

import Instances.{given Ordering[?], given ExecutionContext}

依類型匯入可以與依名稱匯入混合。如果兩者都出現在匯入子句中,依類型匯入會排在最後。例如,這個匯入子句匯入了 imintOrdlistOrd,但遺漏了 ec

import Instances.{im, given Ordering[?]}

一個範例

作為一個具體範例,假設您有這個包含兩個 given 定義的 MonthConversions 物件

object MonthConversions:
  trait MonthConverter[A]:
    def convert(a: A): String

  given intMonthConverter: MonthConverter[Int] with
    def convert(i: Int): String =
      i match
        case 1 =>  "January"
        case 2 =>  "February"
        // more cases here ...

  given stringMonthConverter: MonthConverter[String] with
    def convert(s: String): String =
      s match
        case "jan" => "January"
        case "feb" => "February"
        // more cases here ...

若要將那些 givens 匯入目前的範圍,請使用這兩個 import 陳述

import MonthConversions.*
import MonthConversions.{given MonthConverter[?]}

現在您可以建立一個使用那些 given 執行個體的方法

def genericMonthConverter[A](a: A)(using monthConverter: MonthConverter[A]): String =
  monthConverter.convert(a)

然後您可以在應用程式中使用那個方法

@main def main =
  println(genericMonthConverter(1))       // January
  println(genericMonthConverter("jan"))   // January

如前所述,「匯入 given」語法的關鍵設計優點之一,是讓範圍內的 givens 來源一目瞭然,而且在這些 import 陳述中,很明顯 givens 來自 MonthConversions 物件。

此頁面的貢獻者