使用这个库会越来越觉得麻烦,因为每请求它的一个成员,都需要键入
BizarroMath,但是 Scala 允许将
BizarroMath 的每一个成员导入最高层的词法空间,因此简直就可以把它们当成全局函数来使用(如清单 7所示):
清单 7. 计算 Enron的开支
| package com{ package tedneward { package scala { package demonstration { object App2 { def main(args : Array[String]) : Unit = { import com.tedneward.scala.mathfun.BizarroMath._ System.out.println("2 + 2 = " + bizplus(2,2)) } } } } }} |
还有其他的一些构造很有趣,它们允许 Scala 开发人员写出更自然的
2 bizplus 2,但是这些内容本文不予讨论(想了解 Scala 潜在的可以用于其他用途的特性的读者可以看一下 Odersky、Spoon 和 Venners 所著的
Programming in Scala 中谈到的 Scala
implicit 构造)。
访问
打包(和导入)是 Scala 封装的一部分,和在 Java 代码中一样,在 Scala 中,打包很大一部分在于以选择性方式限定访问特定成员的能力 — 换句话说,在于 Scala 将特定成员标记为 “公有(public)”、“private(私有)” 或介于两者之间的成员的能力。
Java 语言有四个级别的访问:公有(public)、私有(private)、受保护的(protected )和包级别(它没有任何关键词)访问。Scala:
- 废除了包级别的限制(在某种程度上)
- 默认使用 “公有”
- 指定 “私有” 表示 “只有此作用域可访问”
相反,Scala 定义 “protected” 的方式与在 Java 代码中不同;Java protected 成员对于子类和在其中定义成员的包来说是可访问的,Scala 中则仅有子类可访问。这意味着 Scala 版本的 protected 限制性要比 Java 版本更严格(虽然按理说更加直观)。
然而,Scala 真正区别于 Java 代码的地方是 Scala 中的访问修饰符可以用包名来 “限定”,用以表明
直到 哪个访问级别才可以访问成员。例如,如果
BizarroMath 包要将成员访问权限授权给同一包中的其他成员(但不包括子类),可以用清单 8 中的代码来实现:
清单 8. Enron 的记帐代码
| package com{ package tedneward { package scala { // ... package mathfun { object BizarroMath { def bizplus(a : Int, b : Int) = { a - b } def bizminus(a : Int, b : Int) = { a + b } def bizmultiply(a : Int, b : Int) = { a / b } def bizdivide(a : Int, b : Int) = { a * b } private[mathfun] def bizexp(a : Int, b: Int) = 0 } } } }} |
注意此处的
private[mathfun] 表达。本质上,这里的访问修饰符是说该成员
直到 包
mathfun 为止都是私有的;这意味着包
mathfun 的任何成员都有权访问
bizexp,但任何包以外的成员都无权访问它,包括子类。
这一点的强大意义就在于任何包都可以使用 “private” 或者 “protected” 声明甚至
com(乃至
_root_,它是根名称空间的别名,因此本质上
private[_root_] 等效于 “public” 同)进行声明。这使得 Scala 能够为访问规范提供一定程度的灵活性,远远高于 Java 语言所提供的灵活性。
实际上,Scala 提供了一个更高程度的访问规范:
对象私有 规范,用
private[this] 表示,它规定只有被同一对象调用的成员可以访问有关成员,其他对象里的成员都不可以,即使对象的类型相同(这弥合了 Java 访问规范系统中的一个缺口,这个缺口除对 Java 编程问题有用外,别无他用。)
注意访问修饰符必须在某种程度上在 JVM 之上映射,这致使定义中的细枝末节会在从正规 Java 代码中调用或编译时丢失。例如,上面的 BizarroMath 示例(用
private[mathfun] 声明的成员
bizexp)将会生成清单 9 中的类定义(当用 javap 来查看时):
Listing 9. Enron 的记帐库,JVM 视图
| Compiled from "packaging.scala"public final class com.tedneward.scala.mathfun.BizarroMath extends java.lang.Object{ public static final int $tag(); public static final int bizexp(int, int); public static final int bizdivide(int, int); public static final int bizmultiply(int, int); public static final int bizminus(int, int); public static final int bizplus(int, int);} |
在编译的
BizarroMath 类的第二行很容易看出,
bizexp() 方法被赋予了 JVM 级别的
public 访问修饰符,这意味着一旦 Scala 编译器结束访问检查,细微的
private[mathfun] 区别就会丢失。因此,对于那些要从 Java 代码使用的 Scala 代码,我宁愿坚持传统的 “private” 和 “public” 的定义(甚至 “protected” 的定义有时最终映射到 JVM 级别的 “public”,所有不确定的时候,请对照实际编译的字节码参考一下 javap,以确认其访问级别。)
应用
在本系列上一期的文章中(“
集合类型”),当谈及 Scala 中的数组时(确切地说是
Array[T])我说过:“获取数组的第 i 个元素” 实际上是 “那些名称很有趣的方法中的一种……”。尽管当时是因为我不想深入细节,但不管怎么说事实证明这种说法
严格来说 是不对的。
好吧,我承认,我说谎了。
技术上讲,在
Array[T] 类上使用圆括号要比使用 “名称有趣的方法” 复杂一点;Scala 为特殊的字符序列(即那些有左右括号的序列)保留了一个特殊名称关联,因为它有着特殊的使用意图 :“做”……(或按函数来说,将……“应用” 到……)。
换句话说,Scala 有一个特殊的语法(更确切一些,是一个特殊的语法关系)来代替 “应用” 操作符 “()”。更精确地说,当用
() 作为方法调用来调用所述对象时,Scala 将称为
apply() 的方法作为调用的方法。例如,一个想充当
仿函数(functor)的类(一个充当函数的对象)可以定义一个
apply 方法来提供类似于函数或方法的语义:
清单 10. 使用 Functor!
| class ApplyTest{ import org.junit._, Assert._ @Test def simpleApply = { class Functor { def apply() : String = { "Doing something without arguments" } def apply(i : Int) : String = { if (i == 0) "Done" else "Applying... " + apply(i - 1) } } val f = new Functor assertEquals("Doing something without arguments", f() ) assertEquals("Applying... Applying... Applying... Done", f(3)) }} |
好奇的读者会想是什么使仿函数不同于匿名函数或闭包呢?事实证明,它们之间的关系相当明显:标准 Scala 库中的 Function1 类型(指包含一个参数的函数)在其定义上有一个
apply 方法。快速浏览一些为 Scala 匿名函数生成的 Scala 匿名类,您就会明白生成的类是 Function1(或者 Function2 或 Function3,这要看该函数使用了几个参数)的后代。
这意味着当匿名的或者命名的函数不一定适合期望设计方法时,Scala 开发人员可以创建一个
functor 类,提供给它一些初始化数据,保存在字段中,然后通过
() 执行它,无需任何通用基类(传统的策略模式实现需要这个类):
清单 11. 使用 Functor!
| class ApplyTest{ import org.junit._, Assert._ // ... @Test def functorStrategy = { class GoodAdder { def apply(lhs : Int, rhs : Int) : Int = lhs + rhs } class BadAdder(inflateResults : Int) { def apply(lhs : Int, rhs : Int) : Int = lhs + rhs * inflateResults } val calculator = new GoodAdder assertEquals(4, calculator(2, 2)) val enronAccountant = new BadAdder(50) assertEquals(102, enronAccountant(2, 2)) }} |
任何提供了被适当赋予了参数的
apply 方法的类,只要这些参数都按数字和类型排列了起来,它们都会在被调用时运行。
结束语
Scala 的打包、导入和访问修饰符机制提供了传统 Java 编程人员从未享受过的更高级的控制和封装。例如,它们提供了导入一个对象的选择方法的能力,使它们看起来就像全局方法一样,而且还克服了全局方法的传统的缺点;它们使得使用那些方法变得极其简单,尤其是当这些方法提供了诸如本系列早期文章(“
Scala 控制结构内部揭密”)引入的虚构的
tryWithLogging 函数这样的高级功能时。
同样,“应用” 机制允许 Scala 隐藏函数部分的执行细节,这样,编程人员可能会不知道(或不在乎)他们正调用的
东西 事实上不是一个函数,而是一个非常复杂的对象。该机制为 Scala 机制的函数特性提供了另一个方面,当然 Java 语言(或者 C# 或 C++)也提供了这个方面,但是它们提供的语法纯度没有 Scala 的高。
这就是本期的全部内容;在下一期发布前,请尽情欣赏!