Scala 3 — Book

打包和导入

Language

Scala 使用 创建命名空间,让您可以模块化程序并帮助防止命名空间冲突。 Scala 支持 Java 使用的包命名样式,也支持 C++ 和 C# 等语言使用的“花括号”命名空间表示法。

Scala 导入成员的方法也类似于 Java,并且更灵活。 使用 Scala,您可以:

  • 导入包、类、对象、traits 和方法
  • 将导入语句放在任何地方
  • 导入成员时隐藏和重命名成员

这些特性在以下示例中进行了演示。

创建一个包

通过在 Scala 文件的顶部声明一个或多个包名称来创建包。 例如,当您的域名是 acme.com 并且您正在使用名为 myapp 的应用程序中的 model 包中工作时,您的包声明如下所示:

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 部分

导入语句用于访问其他包中的实体。 导入语句分为两大类:

  • 导入类、trait、对象、函数和方法
  • 导入 given 子句

如果您习惯于 Java 之类的语言,则第一类 import 语句与 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 as 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

隐藏多个成员

要在导入过程中隐藏多个成员,请在使用最终通配符导入之前列出它们:

scala> 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:
    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 {
    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 实例

正如您将在 上下文抽象 一章中看到的,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, *}

讨论

通配符选择器 * 将除给定或扩展之外的所有定义带入范围,而 given 选择器将所有给定——包括那些由扩展产生的定义——带入范围。

这些规则有两个主要好处:

  • 范围内的给定来自哪里更清楚。 特别是,不可能在一长串其他通配符导入中隐藏导入的给定。
  • 它可以在不导入任何其他内容的情况下导入所有给定。 这一点特别重要,因为给定可以是匿名的,所以通常使用命名导入是不切实际的。

按类型导入

由于给定可以是匿名的,因此按名称导入它们并不总是可行的,通常使用通配符导入。 按类型导入 为通配符导入提供了更具体的替代方案,这使得导入的内容更加清晰:

import A.{given TC}

这会在 A 中导入任何具有符合 TC 的类型的 given。 导入多种类型的给定 T1,...,Tn 由多个 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}

按类型导入可以与按名称导入混合。 如果两者都存在于导入子句中,则按类型导入排在最后。 例如,这个 import 子句导入了 imintOrdlistOrd,但省略了 ec

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

一个例子

作为一个具体的例子,假设你有这个 MonthConversions 对象,它包含两个 given 定义:

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 ...

要将这些给定导入当前范围,请使用以下两个 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

如前所述, import given 语法的主要设计优势之一是明确范围内的给定来自何处,并且在这些 import 语句中,很清楚地表明给定是来自 MonthConversions 对象。

Contributors to this page: