Scala 3 — 书籍

打包和导入

语言

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

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

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

以下示例演示了这些功能。

创建包

通过在 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 部分

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

  • 导入类、特征、对象、函数和方法
  • 导入 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 实例

正如您将在 上下文抽象 章节中看到的那样,在 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 选择器将所有给定值(包括由扩展产生的给定值)引入范围。

这些规则有两个主要优点

  • 范围内的给定值来自何处更加清晰。特别是,不可能在其他通配符导入的长列表中隐藏导入的给定值。
  • 它支持在不导入任何其他内容的情况下导入所有给定值。这尤其重要,因为给定值可以是匿名的,因此通常不实用使用命名导入。

按类型导入

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

import A.{given TC}

这将导入 A 中的任何 given,其类型符合 TC。通过多个 given 选择器表示导入多种类型 T1,...,Tn 的给定值

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[?]}

一个示例

作为具体示例,假设您有此 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 ...

要将这些 given 导入到当前作用域,请使用这两个 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”语法的关键设计优点之一是明确作用域中 given 的来源,并且在这些 import 语句中很明显,given 来自 MonthConversions 对象。

此页面的贡献者