最近,读者的反馈使我意识到,在编写本系列文章时,我已经遗忘了Scala语言的一个重要方面:Scala的package和access修饰符功能。 因此,在深入探讨该语言的一种更实用的元素之一( apply
机制)之前,我将花点时间进行介绍。
打包
为了以一种彼此不冲突的方式隔离代码,Java™代码提供了package
关键字,创建了一个在其中声明类的词法命名空间。 本质上,将类Foo
放入名为com.tedneward.util的包中会将正式的类名称修改为com.tedneward.util.Foo
; 必须这样引用。 Java程序员会很快指出他们不这样做,而是import
了程序包,从而避免了必须键入正式名称的麻烦。 的确如此,但这仅意味着通过其正式名称引用该类的工作就属于编译器和字节码。 快速浏览一下javap输出就可以发现这种情况。
Java语言的程序包有一些怪癖,但是:程序包声明必须出现在.java文件的顶部,在该文件中出现了程序包作用域的类(在尝试将注释应用到该程序包时会严重破坏该语言。包装); 声明包含整个文件的作用域。 这意味着在极少数情况下,必须跨文件将两个类跨包边界紧密地耦合在一起,从而使那些不知所措的人无法识别两者之间的紧密耦合。
Scala在打包方面采用了稍微不同的方法,将其视为Java语言的declaration
方法和C#的scope
d方法的组合。 考虑到这一点,Java开发人员可以像传统的Java类一样使用传统的Java方法,并将package
声明放在.scala文件的顶部。 包声明就像在Java代码中一样,适用于整个文件范围。 另外,Scala开发人员可以使用Scala的包“作用域”方法,其中大括号界定了package
语句的范围,如清单1所示:
清单1.包装变得简单
package com
{
package tedneward
{
package scala
{
package demonstration
{
object App
{
def main(args : Array[String]) : Unit =
{
System.out.println("Howdy, from packaged code!")
args.foreach((i) => System.out.println("Got " + i) )
}
}
}
}
}
}
实际上,此代码声明了一个类App
,或更确切地说,声明了一个名为com.tedneward.scala.demonstration.App
类。 注意,Scala还允许将包名称用点分隔,因此清单1可以写得更简洁,如清单2所示:
清单2.包装变得简单(redux)
package com.tedneward.scala.demonstration
{
object App
{
def main(args : Array[String]) : Unit =
{
System.out.println("Howdy, from packaged code!")
args.foreach((i) => System.out.println("Got " + i) )
}
}
}
使用哪种样式似乎更合适,因为它们每个都可以编译为完全相同的代码结构。 (scalac编译会继续进行,并像javac一样在程序包声明的子目录中生成.class文件。)
进口
当然,打包的逻辑反面是import
,Scala的将名称带入当前词法命名空间的机制。 该系列的读者之前已经在几个示例中看到了import
,但是现在我该指出一些import
的功能了,这些功能会让Java开发人员感到惊讶。
首先,您可以在客户端Scala文件中的任何位置使用import
,而不仅仅是在文件的顶部,并且具有相应的作用域。 因此,在清单3中, java.math.BigInteger
导入的作用范围完全是对象App
内定义的方法,而没有其他地方。 如果mathfun
内部的另一个类或对象想要使用java.math.BigInteger
,则需要像App
一样导入该类。 或者,如果mathfun
几个类都希望使用java.math.BigInteger
,则导入可以在App
定义之外的包级别进行,并且此包范围中的所有类都将导入BigInteger
。
清单3.导入范围
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == BigInteger.ZERO) BigInteger.ONE
else arg multiply (factorial (arg subtract BigInteger.ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
但是,导入不止于此。 Scala认为没有真正的理由区分顶级成员和嵌套成员,因此您可以使用import
不仅将嵌套类型带入词法作用域,还可以将任何成员带入词法范围。 例如,通过在java.math.BigInteger
导入所有名称,可以将对ZERO和ONE的作用域引用删除,以仅命名引用,如清单4所示:
清单4.静态导入...没有静态
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger
import BigInteger._
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == ZERO) ONE
else arg multiply (factorial (arg subtract ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
通过使用下划线(还记得Scala中的通配符吗?),您可以有效地告诉Scala编译器应该将BigInteger
内部的所有成员都纳入范围。 并且由于BigInteger
已被前面的import语句置于范围内,因此无需显式对类名进行包装限定。 实际上,这些甚至可以合并为一个语句,因为import
可以采用多个逗号分隔的目标来导入(如清单5所示):
清单5.批量导入
package com
{
package tedneward
{
package scala
{
// ...
package mathfun
{
object App
{
import java.math.BigInteger, BigInteger._
def factorial(arg : BigInteger) : BigInteger =
{
if (arg == ZERO) ONE
else arg multiply (factorial (arg subtract ONE))
}
def main(args : Array[String]) : Unit =
{
if (args.length > 0)
System.out.println("factorial " + args(0) +
" = " + factorial(new BigInteger(args(0))))
else
System.out.println("factorial 0 = 1")
}
}
}
}
}
}
这样可以节省一两行。 请注意,这两个不能合并:第一个导入BigInteger
类本身,第二个导入该第一类内部的各种成员。
您还可以使用import
来引入其他非常数成员。 例如,考虑清单6中的数学实用程序库(也许价值可疑,但仍然...):
清单6. 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 }
}
}
}
}
}
随着时间的流逝,使用此库可能会很烦人,每次需要请求BizarroMath
的成员时都必须键入BizarroMath
,但是Scala允许BizarroMath
每个成员都导入顶级词汇名称空间,就像它们是全局的一样。函数(如清单7所示):
清单7.计算安然的费用
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 在Scala编程中所涵盖的Scala implicit
构造。)
访问
尽管打包(和导入)是Scala封装和打包故事的一部分,但与Java代码一样,打包的很大一部分在于它能够以选择性的方式限制对某些成员的访问,换句话说,就是Scala的能力。将某些成员标记为“公开”,“私有”或介于两者之间。
Java语言具有四个访问级别:公共,私有,受保护和程序包级别的访问(通过省略任何关键字而令人沮丧地应用)。 Scala:
- 取消了包级资格(以某种方式)
- 默认使用“公共”
- 指定“专用”表示“仅可在此范围内访问”
相比之下,“受保护”绝对不同于Java代码中的“受保护”。 在受Java保护的成员可以访问子类和定义该成员的包的情况下,Scala选择仅授予对子类的访问权限。 这意味着Scala的protected版本比Java版本更具限制性(尽管可以说更直观)。
当斯卡拉真正几步从Java代码了,然而,就是在斯卡拉访问修饰符可以是“合格”与包名,表示访问级别达到该成员可以访问。 例如,如果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]
的private[mathfun]
表达式。 从本质上说,访问修饰符是说该成员是私有到包mathfun
; 这意味着包mathfun
任何成员都可以访问bizexp
但该包之外的任何内容都不能访问,包括子类。
强大的含义是,任何包都可以在com
的“ private”或“ protected”声明中进行声明(甚至是root命名空间的别名_root_
,从而使private[_root_]
相同)称为“公共”)。 这为访问规范提供了一定程度的灵活性,远远超出了Java语言提供的灵活性。
实际上,Scala提供了更高级别的访问规范:由private[this]
说明的对象私有规范,该规范规定,只有在同一对象上调用的成员才能看到所讨论的成员,即使来自不同对象,甚至如果它们是同一类型。 (这填补了Java访问规范系统中的一个小漏洞,该漏洞对于Java编程面试问题很有用,而其他方面无济于事。)
请注意,访问修饰符必须在某种程度上映射到JVM之上,结果,当从常规Java代码编译或调用它们时,其定义中的一些细微差别将丢失。 例如,上面的BizarroMath示例(带有private[mathfun]
声明的成员bizexp
)将生成清单9中的类定义(当使用javap进行查看时):
清单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
类的第二行可以明显BizarroMath
,为bizexp()
方法提供了JVM级的public
访问指定符,这意味着一旦Scala编译器完成访问检查,就失去了微妙的private[mathfun]
区别。 。 因此,对于打算从Java代码中使用的Scala代码,我宁愿坚持使用传统的“私有”和“公共”定义。 (即使“受保护的”有时也会最终映射到JVM级别的“公共”,因此,如果有疑问,请针对实际的编译字节码咨询javap以确定其访问级别。)
应用
当谈论Scala中的Array[T]
确切地说是Array[T]
)时,获得数组的第i个元素实际上是“这些方法中另一个有趣的名称...。”事实证明,尽管我没有“T想进入细节的话,这是不完全正确的。
好,我承认,我撒了谎。
从技术上讲,对Array[T]
类使用括号要比仅使用“带有有趣名称的方法”复杂一些。 Scala为该特定字符序列(即左parens-right-parens序列)保留了一个特定的命名法关联,因为这种用法经常带有特定的意图:“做某事”(或在功能上讲“将“应用于某物”。
换句话说,Scala对于“应用程序”运算符“()”具有特殊的语法(更准确地说,是特殊的句法关系)。 确切地说,当使用()
作为方法调用来调用所述对象时,Scala将称为apply()
的方法识别为要调用的方法。 例如,想要充当函子 (充当函数的对象)的类可以定义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程序员从未享受过的出色的控制和封装。 例如,它们提供了导入对象的选择方法的能力,使它们显示为全局方法,而没有全局方法的传统缺点; 它们使使用这些方法非常容易,特别是如果那些方法是提供高阶功能的方法,例如本系列前面介绍的虚构tryWithLogging
函数(“ 不要抛出循环! ”)。
类似地,“应用程序”机制允许Scala将执行细节隐藏在功能幕墙之后,以使程序员甚至可能不知道(或不在乎)他们所调用的东西实际上不是一个函数,而是一个复杂得多的对象。 该机制为Scala的功能特性提供了另一种维度,当然可以从Java语言(或C#或C ++)实现这一功能,但与Scala所提供的语法纯度无关。
这是本期的内容; 直到下一次,尽情享受!
翻译自: https://www.ibm.com/developerworks/java/library/j-scala07298/index.html