包常用于构建相关联的代码模块,并用于避免命名空间冲突。通常情况下,可以用和Java相同的格式创建Scala的包,所以大部分Scala源代码文件均以package声明开头,如下所示:
package com.alvinalexander.myapp.model
class Person ...
然而,Scala语法更加灵活,除此之外,你还可以使用花括号的包风格,这类似于C++和C#的命名空间。随后的9.1小节中会有该语法的展示。
Scala导入成员的方法与Java类似,而且更灵活。 在Scala里可以:
- 随处使用import语句。
- 导入类、包或者对象。
- 在导入成员时隐藏和重命名成员。
本章展示了所有的上述方法。
在深入了解这些小节之前,你需要注意Scala默认会有两个包被隐式导入到所有源代码文件的作用域中:
- java.lang.*
- scala.*
在Scala 3中,import语句中的 * 字符类似于 Java 中的 * 字符,因此这些语句表示“导入包中的每个成员”。
除了这两个包之外,来自 scala.Predef 对象的所有成员也被隐式导入到源代码文件中。
如果想了解Scala的工作原理,强烈建议花点时间深入研究Predef 对象( https://oreil.ly/KtxXV )的源码。虽然代码不长,但它展示了Scala语言的很多特性。
正如我在“这些方法从何而来?”的讨论中所说,隐式转换被Predef对象引入到作用域中,在Scala 2.13的Predef对象在Scala 3.0仍在使用,代码如下所示:
implicit def long2Long(x: Long): java.lang.Long = x.asInstanceOf[java.lang.Long]
implicit def Long2long(x: java.lang.Long): Long = x.asInstanceOf[Long]
// more implicit conversions ...
同样,如果想知道为什么可以在不需要 import 语句的情况下调用 Map、Set 和 println 的代码,也可以在 Predef 中找到这些代码:
type Map[A, +B] = immutable.Map[A, B]
type Set[A] = immutable.Set[A]
def println(x: Any) = Console.println(x)
def printf(text: String, xs: Any*) = Console.print(text.format(xs: _*))
def assert(assertion: Boolean) { ... }
def require(requirement: Boolean) { ... }
你想使用嵌套风格的包表示法,类似于 C++ 和 C# 中的命名空间表示法。
提供包名的同时,将一个或多个类包装在一对花括号内,如下所示:
package com.acme.store {
class Foo:
override def toString = "I am com.acme.store.Foo"
}
这个类的规范名称是com.acme.store.Foo。 和这样声明代码是一样的:
package com.acme.store
class Foo:
override def toString = "I am com.acme.store.Foo"
使用这种方法,可以将多个包放在一个文件中,也可以创建嵌套包。 为了展示这两种方法,以下例子创建了三个Foo类,都位于不同的包中:
package orderentry {
class Foo:
override def toString = "I am orderentry.Foo"
}
package customers {
class Foo:
override def toString = "I am customers.Foo"
package database {
class Foo:
override def toString = "I am customers.database.Foo"
}
}
// the output is shown after the comment tags.
@main def packageTests =
println(orderentry.Foo()) // I am orderentry.Foo
println(customers.Foo()) // I am customers.Foo
println(customers.database.Foo()) // I am customers.database.Foo
这表明每个Foo类都在不同的包中,并且database包嵌套在customers包中。
我看过很多Scala代码,据我所知,在文件顶部声明包名是最流行的包风格:
package foo.bar.baz
class Foo:
override def toString = "I'm foo.bar.baz.Foo"
但是,由于Scala代码可以非常简洁,如果想在一个文件中声明多个类和包时,另一种花括号打包的语法会很方便。 比如在本书的源码仓库( https://github.com/alvinj/ScalaCookbookV2Examples )中,会看到我经常使用这种风格。
有时查看Scala程序时,会在源码文件的顶部看到多个包声明,如下所示:
package com.alvinalexander
package tests
...
这和编写两个嵌套的包的代码完全相同,如下所示:
package com.alvinalexander {
package tests {
...
}
}
使用第一种形式的原因是Scala程序员通常不喜欢使用花括号样式缩进代码,特别是在大文件中。所以他们用第一种形式。
如果使用两个包子句而不是一个,则和每种方式在当前作用域中的可用性有关。而如果只使用一个包语句:
package com.alvinalexander.tests
然而只有com.alvinalexander.tests的成员被引入作用域。但如果使用两个包声明:
package com.alvinalexander
package tests
...
com.alvinalexander 和 com.alvinalexander.tests 的成员都被引入作用域。
之所以采用这种方法,与Scala 2.7中发现的并在Scala 2.8中解决的一个情况有关。详细信息可参阅Martin Odersky文章 chained package clauses( https://oreil.ly/YNvjN )。
你想将一个或多个成员导入当前代码的作用域。
用这样的语法导入一个类:
import java.io.File
还可以像这样导入多个类:
import java.io.File
import java.io.IOException
import java.io.FileNotFoundException
更简洁的像这样:
import java.io.{File, IOException, FileNotFoundException}
我将其称为花括号语法,但更正式地称为导入选择子句。
这样导入java.io 包中所有内容:
import java.io.*
Scala的语法很灵活,你可以:
- 将 import 语句放在任何地方,包括类的顶部、类或对象内、方法内或代码块内。 该技术将在9.6小节展示。
- 导入包、类、对象和方法。
- 导入成员时隐藏和重命名成员。 9.3小节和9.4小节中展示这些技术。
你想在导入时重命名成员,以避免命名空间冲突或混淆。
使用以下语法导入时,为导入的类指定一个新名字:
import java.awt.{List as AwtList}
然后在代码中,通过别名来引用这个类:
scala> val alist = AwtList(1, false)
val alist: java.awt.List = java.awt.List[list0,0,0,0x0,invalid,selected=null]
通过AwtList来使用java.awt.List类,也可以通过惯用名使用Scala的List类:
scala> val x = List(1, 2, 3)
val x: List[Int] = List(1, 2, 3)
可以在导入的时候重命名多个类:
import java.util.{Date as JDate, HashMap as JHashMap}
还可以在导入的最后位置使用 * 字符从而导入其他所有内容(无需重命名其他成员):
import java.util.{Date as JDate, HashMap as JHashMap, *}
在导入的时候创建了别名,所以不能在代码中使用类的原始(真实)名字。 在使用最后一个 import 语句后,下面代码将失败,编译器找不到 java.util.HashMap 类,因为被重命名:
scala> val map = HashMap[String, String]()
<console>:12: error: not found: type HashMap
val map = HashMap[String, String]
^
正如预期的那样失败了,但是可以用别名来引用这个类:
scala> val map = JHashMap[String, String]()
map: java.util.HashMap[String,String] = {}
由于 import 语句末尾的 * 从 java.util 包中导入了其余所有内容,所以别的 java.util 类的代码可以使用:
scala> val x = ArrayList[String]()
x: java.util.ArrayList[String] = []
scala> val y = LinkedList[String]()
y: java.util.LinkedList[String] = []
如上所示,在导入时为类创建新名字,在用新名字或别名来引用类。 Programming in Scala 将这种做法称为renaming clause。
这样做有助于避免命名空间冲突和混淆。如Listener、Message、Handler、Client、Server 这些类的名字很常见,在导入时重命名会很有帮助。
Scala 3的语法与Scala 2不同,以下代码展示了Scala 3 与Scala 2的区别:
// scala 2
import java.util.{Date => JDate, HashMap => JHashMap, _}
// scala 3
import java.util.{Date as JDate, HashMap as JHashMap, *}
在编写本章时,仍可以在 Scala 3代码中使用Scala 2语法,但下划线的语法最终会被淘汰,所以优先使用新语法。
这些有趣的技巧组合,不仅可以在导入时重命名类,也可以重命名类的成员和Java静态成员,在下面的脚本中,println被重命名为更短的名字,如REPL中所示:
scala> import System.out.{println as p}
scala> p("hello")
hello
因为out是PrintStream,java.lang.System中一个的static final实例,而println是PrintStream的方法。最终结果是,p是println方法的别名。
为了避免命名冲突或混淆,你想在引入来自同一个包的其他成员时,隐藏一个或多个类。
导入时隐藏类可以使用9.3小节重命名的语法,但需要把类名指向字符 _,以下例子在导入java.util包中所有成员时隐藏了Random类:
import java.util.{Random => _, *}
在REPL中运行验证:
scala> import java.util.{Random => _, *}
import java.util.{Random=>_, _}
// can’t access Random
scala> val r = Random()
1 |val r = Random()
| ^^
| Not found: Random
// can access other members
scala> val x = ArrayList()
val x: java.util.ArrayList[Nothing] = []
在这个例子中,下面代码隐藏了Random类:
import java.util.{Random => _}
之后,大括号内的 * 字符就相当于说明你要导入包中的其他所有内容,像这样:
import java.util.*
注意导入通配符 * 必须在最后一个位置。 在其他位置会出错:
scala> import java.util.{*, Random => _}
1 |import java.util.{*, Random => _}
| ^^
| named imports cannot follow wildcard imports
这是因为导入中要隐藏多个成员,得先列出它们。
导入时,在最后的通配符前列出要隐藏的成员:
import java.util.{List => _, Map => _, Set => _, *}
在这个导入语句之后,可以使用 java.util 中的其他类:
scala> val x = ArrayList[String]()
val x: java.util.ArrayList[String] = []
你仍可以使用Scala的 List、Set 和 Map 类,而不会和同名的java.util中的类发生命名冲突:
// these are all Scala classes
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)
当使用 * 通配符导入包的多个成员时,但因为命名冲突,需要隐藏一个或多个成员时,这种方式很有用。
你想用类似Java静态导入的方式导入成员,这样可以直接引用成员名,而不用在前面加上包名或类名。
通过名字或Scala的 * 通配符导入静态成员。从 scala.math 包中导入静态 cos 方法:
import scala.math.cos
val x = cos(0) // 1.0
从 scala.math 包中导入所有成员:
import scala.math.*
这种语法可以访问 scala.math 包的所有静态成员,而不用在前面加上类名:
import scala.math.*
val a = sin(0) // Double = 0.0
val b = cos(Pi) // Double = −1.0
Java的 Color 类也展示了该技术的好处:
import java.awt.Color.*
println(RED) // java.awt.Color[r=255,g=0,b=0]
println(BLUE) // java.awt.Color[r=0,g=0,b=255]
该技术的一个常见示例是对象和枚举类型。例如下面 StringUtils 对象:
object StringUtils:
def truncate(s: String, length: Int): String = s.take(length)
def leftTrim(s: String): String = s.replaceAll("^\\s+", "")
可以这样导入和使用方法:
import StringUtils.*
truncate("four score and seven ", 4) // "four"
leftTrim(" four score and ") // "four score and "
同样的,Scala3的枚举:
package days {
enum Day:
case Sunday, Monday, Tuesday, Wednesday,
Thursday, Friday, Saturday
}
可以这样导入和使用枚举:
// a different package
package bar {
import days.Day.*
@main def enumImportTest =
val date = Sunday
// more code here ...
if date == Saturday || date == Sunday then
println("It’s the weekend!")
}
有些开发人员不喜欢静态导入,我却觉得这样让枚举更加可读,相反,在一个常量前加上类名或枚举名会降低代码的可读性:
if date == Day.Saturday || date == Day.Sunday then
println("It’s the weekend!")
使用静态导入,代码中不需要以“Day.”开头,反而更容易阅读:
if date == Saturday || date == Sunday then ...
你想在任何地方都可以使用import语句,而不仅仅是文件顶部。通常为了限制导入的范围,而使代码更清晰。
可以将import语句放在程序任何地方。和Java以及其他语言一样,常见用法在类的顶部导入成员,接着在之后的代码中使用这些导入资源:
package foo
import scala.util.Random
class MyClass:
def printRandom =
val r = Random() //use the imported class
要获得更多控制,可以在类内部导入成员:
package foo
class ClassA: //inside ClassA
import scala.util.Random //inside ClassA
def printRandom =
val r = Random ()
class ClassB:
// the import is not visible here
val r = Random () //error: not found: Random
这样import的作用域被限制在导入语句之后ClassA内的代码。
可以在方法中使用 import 语句:
def getPandoraItem(): Any =
import com.alvinalexander.pandorasbox.*
val p = Pandora()
p.getRandomItem
甚至可以把import语句放在代码块中,并将其作用域限制在import语句后的代码。下面例子正确声明了r1,因为它在代码块中且在import 语句后,但字段 r2 的声明不能通过编译,因为没有正确的引入Radom类:
def printRandom =
{
import scala.util.Random
val r1 = Random() //this works, as expected
}
val r2 = Random() //error: not found: Random
import语句使导入的成员在导入后才可用,这也限制了其作用域。下面代码无法通过编译,因为在import 语句之前引用Random类:
// this does not compile
class ImportTests:
def printRandom =
val r = Random() //error: not found: type Random
import scala.util.Random
当一个文件中包含多个类和包时,可以结合 import 语句和花括号风格的包方式(如9.1小节所示),用以限制 import 语句的范围,如下所示:
package orderentry {
import foo.*
// more code here ...
}
package customers {
import bar.*
// more code here ...
package database {
import baz.*
// more code here ...
}
}
这个例子中成员访问方法如下:
- orderentry包中的代码可以访问foo的成员,但无法访问bar或baz的成员。
- customers和customer.database中的代码不能访问foo的成员。
- customers的代码可以访问bar的成员。
- customers.database的代码可以访问bar和baz中的成员。
同样的概念适用于一个文件中定义多个类:
package foo
// available to all classes defined below
import java.io.File
import java.io.PrintWriter
class Foo:
// only available inside this class
import javax.swing.JFrame
// ...
class Bar:
// only available inside this class
import scala.util.Random
// ...
尽管在文件的顶部或者只是在使用前加入import语句是一种风格,但我发现在一个文件中有多个类或包时,这种灵活性显得很有用。在这些情况下,最好将导入保持在尽可能小的作用域内,从而限制命名空间冲突,且在代码在增长时更容易重构。
你需要将一个或多个given实例导入到当前作用域,同时也可能从同一个包中导入类型。
given实例简称given,通常在单独的模块中定义,且必须使用特殊的 import 语句将其导入当前作用域。例如在包名为co.kbhr.givens,对象名为Addr的given代码中:
package co.kbhr.givens
object Adder:
trait Adder[T]:
def add(a: T, b: T): T
given intAdder: Adder[Int] with
def add(a: Int, b: Int): Int = a + b
使用两个 import 语句将其导入当前作用域:
@main def givenImports =
import co.kbhr.givens.Adder.* // import all nongiven definitions
import co.kbhr.givens.Adder.given // import all `given` definitions
def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = adder.add(x, y)
println(genericAdder(1, 1))
也可以将两个import语句合二为一:
import co.kbhr.givens.Adder.{given, *}
可以按类型导入匿名given实例,如本例中的第二个import语句所示:
package co.kbhr.givens
object Adder:
trait Adder[T]:
def add(a: T, b: T): T
given Adder[Int] with
def add(a: Int, b: Int): Int = a + b
given Adder[String] with
def add(a: String, b: String): String = "" + (a.toInt + b.toInt)
@main def givenImports =
// when put on separate lines, the order of the imports is important.
// the second import statement imports the givens by their type.
import co.kbhr.givens.Adder.*
import co.kbhr.givens.Adder.{given Adder[Int], given Adder[String]}
def genericAdder[A](x: A, y: A)(using adder: Adder[A]): A = adder.add(x, y)
println(genericAdder(1, 1)) // 2
println(genericAdder("2", "2")) // 4
该例中,这两行代码展示了如何导入Addr特质和given:
import co.kbhr.givens.Adder.*
import co.kbhr.givens.Adder.{given Adder[Int], given Adder[String]}
根据所需也可以按类型导入given,如下所示:
import co.kbhr.givens.Adder.*
import co.kbhr.givens.Adder.{given Adder[?]}
第二行可以理解为:“导入任意类型的Addr given,如Addr[Int]或Addr[String]”
根据Scala 3 文档关于导入given( https://oreil.ly/aobrq )的描述,新语法有两个原因和好处:
- 更清楚地说明了作用域内的given从何而来。
- 可以导入所有given而不导入其他任何东西。
given实例可以替换Scala 2中使用的implicits。如上所述,given比implicits更清晰。 given 的动机之一,尤其是given 导入语句,在Scala 2中并不总是清楚implicits是如何进入当前作用域。
Scala 3 中使用 given 解决了这种情况,且创建了新的 import given 语法。正如示例所见,现在可以很容易地查看given语句列表,从而知道given的来源。
- 有关如何使用 given的更多内容,请参阅23.8小节“使用given和using的术语推断”。
- 有关given的更多内容,请参阅Scala 3文档:given实例( https://oreil.ly/5rep7 )。
- 有关导入given的更多内容,请参阅Scala 3文档:导入given( https://oreil.ly/aobrq )。
- Scala 3 contextual抽象的文档( https://oreil.ly/c2IYn )详细说明了从implicits到given实例变化背后的动机。