9. 包和导入
包常用于构建相关联的代码模块,并用于避免命名空间冲突。通常情况下,可以用和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 中的 * 字符,因此这些语句表示“导入包中的每个成员”。
Predef对象
除了这两个包之外,来自 scala.Predef 对象的所有成员也被隐式导入到源代码文件中。
如果想了解Scala的工作原理,强烈建议花点时间深入研究Predef 对象( https://oreil.ly/KtxXV )的源码。虽然代码不长,但它展示了Scala语言的很多特性。
正如我在“这些方法从何而来?”的讨论中所说,隐式转换被Predef对象引入到作用域中,在Scala 2.13的Predef对象在Scala 3.0仍在使用,代码如下所示:
同样,如果想知道为什么可以在不需要 import 语句的情况下调用 Map、Set 和 println 的代码,也可以在 Predef 中找到这些代码:
9.1 花括号风格的包记号法
问题
你想使用嵌套风格的包表示法,类似于 C++ 和 C# 中的命名空间表示法。
解决方案
提供包名的同时,将一个或多个类包装在一对花括号内,如下所示:
这个类的规范名称是com.acme.store.Foo。 和这样声明代码是一样的:
好处
使用这种方法,可以将多个包放在一个文件中,也可以创建嵌套包。 为了展示这两种方法,以下例子创建了三个Foo类,都位于不同的包中:
这表明每个Foo类都在不同的包中,并且database包嵌套在customers包中。
讨论
我看过很多Scala代码,据我所知,在文件顶部声明包名是最流行的包风格:
但是,由于Scala代码可以非常简洁,如果想在一个文件中声明多个类和包时,另一种花括号打包的语法会很方便。 比如在本书的源码仓库( https://github.com/alvinj/ScalaCookbookV2Examples )中,会看到我经常使用这种风格。
链式包子句
有时查看Scala程序时,会在源码文件的顶部看到多个包声明,如下所示:
这和编写两个嵌套的包的代码完全相同,如下所示:
使用第一种形式的原因是Scala程序员通常不喜欢使用花括号样式缩进代码,特别是在大文件中。所以他们用第一种形式。
如果使用两个包子句而不是一个,则和每种方式在当前作用域中的可用性有关。而如果只使用一个包语句:
然而只有com.alvinalexander.tests的成员被引入作用域。但如果使用两个包声明:
com.alvinalexander 和 com.alvinalexander.tests 的成员都被引入作用域。
之所以采用这种方法,与Scala 2.7中发现的并在Scala 2.8中解决的一个情况有关。详细信息可参阅Martin Odersky文章 chained package clauses( https://oreil.ly/YNvjN )。
9.2 导入一个或多个成员
问题
你想将一个或多个成员导入当前代码的作用域。
解决方案
用这样的语法导入一个类:
还可以像这样导入多个类:
更简洁的像这样:
我将其称为花括号语法,但更正式地称为导入选择子句。
这样导入java.io 包中所有内容:
讨论
Scala的语法很灵活,你可以:
将 import 语句放在任何地方,包括类的顶部、类或对象内、方法内或代码块内。 该技术将在9.6小节展示。
导入包、类、对象和方法。
导入成员时隐藏和重命名成员。 9.3小节和9.4小节中展示这些技术。
9.3 导入时重命名成员
问题
你想在导入时重命名成员,以避免命名空间冲突或混淆。
解决方案
使用以下语法导入时,为导入的类指定一个新名字:
然后在代码中,通过别名来引用这个类:
通过AwtList来使用java.awt.List类,也可以通过惯用名使用Scala的List类:
可以在导入的时候重命名多个类:
还可以在导入的最后位置使用 * 字符从而导入其他所有内容(无需重命名其他成员):
在导入的时候创建了别名,所以不能在代码中使用类的原始(真实)名字。 在使用最后一个 import 语句后,下面代码将失败,编译器找不到 java.util.HashMap 类,因为被重命名:
正如预期的那样失败了,但是可以用别名来引用这个类:
由于 import 语句末尾的 * 从 java.util 包中导入了其余所有内容,所以别的 java.util 类的代码可以使用:
讨论
如上所示,在导入时为类创建新名字,在用新名字或别名来引用类。 Programming in Scala 将这种做法称为renaming clause。
这样做有助于避免命名空间冲突和混淆。如Listener、Message、Handler、Client、Server 这些类的名字很常见,在导入时重命名会很有帮助。
Scala 3的语法与Scala 2不同,以下代码展示了Scala 3 与Scala 2的区别:
在编写本章时,仍可以在 Scala 3代码中使用Scala 2语法,但下划线的语法最终会被淘汰,所以优先使用新语法。
这些有趣的技巧组合,不仅可以在导入时重命名类,也可以重命名类的成员和Java静态成员,在下面的脚本中,println被重命名为更短的名字,如REPL中所示:
因为out是PrintStream,java.lang.System中一个的static final实例,而println是PrintStream的方法。最终结果是,p是println方法的别名。
9.4 导入时隐藏类
问题
为了避免命名冲突或混淆,你想在引入来自同一个包的其他成员时,隐藏一个或多个类。
解决方案
导入时隐藏类可以使用9.3小节重命名的语法,但需要把类名指向字符 _,以下例子在导入java.util包中所有成员时隐藏了Random类:
在REPL中运行验证:
讨论
在这个例子中,下面代码隐藏了Random类:
之后,大括号内的 * 字符就相当于说明你要导入包中的其他所有内容,像这样:
注意导入通配符 * 必须在最后一个位置。 在其他位置会出错:
这是因为导入中要隐藏多个成员,得先列出它们。
导入时,在最后的通配符前列出要隐藏的成员:
在这个导入语句之后,可以使用 java.util 中的其他类:
你仍可以使用Scala的 List、Set 和 Map 类,而不会和同名的java.util中的类发生命名冲突:
当使用 * 通配符导入包的多个成员时,但因为命名冲突,需要隐藏一个或多个成员时,这种方式很有用。
9.5 导入静态成员
问题
你想用类似Java静态导入的方式导入成员,这样可以直接引用成员名,而不用在前面加上包名或类名。
解决方案
通过名字或Scala的 * 通配符导入静态成员。从 scala.math 包中导入静态 cos 方法:
从 scala.math 包中导入所有成员:
这种语法可以访问 scala.math 包的所有静态成员,而不用在前面加上类名:
Java的 Color 类也展示了该技术的好处:
讨论
该技术的一个常见示例是对象和枚举类型。例如下面 StringUtils 对象:
可以这样导入和使用方法:
同样的,Scala3的枚举:
可以这样导入和使用枚举:
有些开发人员不喜欢静态导入,我却觉得这样让枚举更加可读,相反,在一个常量前加上类名或枚举名会降低代码的可读性:
使用静态导入,代码中不需要以“Day.”开头,反而更容易阅读:
9.6 在任何地方使用导入语句
问题
你想在任何地方都可以使用import语句,而不仅仅是文件顶部。通常为了限制导入的范围,而使代码更清晰。
解决方案
可以将import语句放在程序任何地方。和Java以及其他语言一样,常见用法在类的顶部导入成员,接着在之后的代码中使用这些导入资源:
要获得更多控制,可以在类内部导入成员:
这样import的作用域被限制在导入语句之后ClassA内的代码。
可以在方法中使用 import 语句:
甚至可以把import语句放在代码块中,并将其作用域限制在import语句后的代码。下面例子正确声明了r1,因为它在代码块中且在import 语句后,但字段 r2 的声明不能通过编译,因为没有正确的引入Radom类:
讨论
import语句使导入的成员在导入后才可用,这也限制了其作用域。下面代码无法通过编译,因为在import 语句之前引用Random类:
当一个文件中包含多个类和包时,可以结合 import 语句和花括号风格的包方式(如9.1小节所示),用以限制 import 语句的范围,如下所示:
这个例子中成员访问方法如下:
orderentry包中的代码可以访问foo的成员,但无法访问bar或baz的成员。
customers和customer.database中的代码不能访问foo的成员。
customers的代码可以访问bar的成员。
customers.database的代码可以访问bar和baz中的成员。
同样的概念适用于一个文件中定义多个类:
尽管在文件的顶部或者只是在使用前加入import语句是一种风格,但我发现在一个文件中有多个类或包时,这种灵活性显得很有用。在这些情况下,最好将导入保持在尽可能小的作用域内,从而限制命名空间冲突,且在代码在增长时更容易重构。
9.7 导入given
问题
你需要将一个或多个given实例导入到当前作用域,同时也可能从同一个包中导入类型。
解决方案
given实例简称given,通常在单独的模块中定义,且必须使用特殊的 import 语句将其导入当前作用域。例如在包名为co.kbhr.givens,对象名为Addr的given代码中:
使用两个 import 语句将其导入当前作用域:
也可以将两个import语句合二为一:
可以按类型导入匿名given实例,如本例中的第二个import语句所示:
该例中,这两行代码展示了如何导入Addr特质和given:
根据所需也可以按类型导入given,如下所示:
第二行可以理解为:“导入任意类型的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实例变化背后的动机。
最后更新于
这有帮助吗?