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, *}
该代码行表示:“重命名 Date
和 HashMap
类,如所示,并导入 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 List
、Set
和 Map
类,而不会出现命名冲突
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
类方法,如 sin
和 cos
,而无需在它们前面加上类名称
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
的成员也默认导入。
如果您曾经想知道为什么可以使用
List
、Vector
、Map
等类而无需导入它们,这是因为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
在此代码中,对象 B
的 import 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]
此导入语句导入了 intOrd
、listOrd
和 ec
实例,但省略了 im
实例,因为它不符合任何指定边界
import Instances.{given Ordering[?], given ExecutionContext}
按类型导入可以与按名称导入混合使用。如果导入子句中同时存在这两种导入,则按类型导入位于最后。例如,此导入子句导入了 im
、intOrd
和 listOrd
,但省略了 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
对象。