内部类(INNER CLASSES)
Scala 中的类可以把其它类作为自己的成员。与内部类是封闭类(enclosing class)成员的 Java 语言相反,Scala 中的内部类被绑定在外部对象上。假设我们希望编译器在编译时能阻止我们,混合哪些节点属于哪个图。路径依赖类型(Path-dependent types)为此提供了一个解决办法。
为了说明这个差异,我们快速书写出图形数据类型的实现:
class Graph {
class Node {
var connectedNodes: List[Node] = Nil
def connectTo(node: Node) {
if (connectedNodes.find(node.equals).isEmpty) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}
该程序将图形表示为节点列表(List[Node]
)。每个节点都有一个连接到其它节点的列表(connectedNodes
)。class Node
是路径依赖类型,因为它嵌套在 class Graph
中。因此,connectedNodes
中的所有节点都必须使用来自 Graph
相同实例的 newNode
来创建。
val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
val node3: graph1.Node = graph1.newNode
node1.connectTo(node2)
node3.connectTo(node1)
为了更加清晰,我们已经明确声明了 node1
,node2
,node3
的类型为 graph1.Node
,但是要知道编译器可以推断出它们的类型。这是因为当我们调用叫作 new Node
的 graph1.newNode
时, 该方法使用特定于实例 graph1
的实例 node
。
如果我们现在有两张图,那么 Scala 类型系统不允许我们将一个图中定义的节点与另一个图中定义的节点混合,因为不同图的节点具有不同的类型。
下面是个有误的程序:
val graph1: Graph = new Graph
val node1: graph1.Node = graph1.newNode
val node2: graph1.Node = graph1.newNode
node1.connectTo(node2) // legal
val graph2: Graph = new Graph
val node3: graph2.Node = graph2.newNode
node1.connectTo(node3) // illegal!
编译报错:Type mismatch, expected: graph1.Node, actual: graph2.Node
(类型不匹配,期望:graph1.Node
,实际:graph2.Node
)
graph1.Node
类型不同于 graph2.Node
类型。在 Java 中,上面例子中的最后一行程序将会是正确的。对于这两个图的节点来说,Java 将分配相同的 Graph Node
类型;即 Node
的前缀是 Graph
类。Scala 里也可以表示这种类型,它被写为 Graph#Node
。如果我们想连接不同图的节点,那我们必须按照以下的方式更改初始图形实现的定义:
class Graph {
class Node {
var connectedNodes: List[Graph#Node] = Nil
def connectTo(node: Graph#Node) {
if (connectedNodes.find(node.equals).isEmpty) {
connectedNodes = node :: connectedNodes
}
}
}
var nodes: List[Node] = Nil
def newNode: Node = {
val res = new Node
nodes = res :: nodes
res
}
}
请注意该程序不允许我们将一个节点附加到两个不同的图形中。如果我们也想除去这种限制,我们必须要修改变量节点 nodes
的类型为 Graph#Node
。
复合类型(COMPOUND TYPES)
有时需要表达一个对象类型是其它几个类型的子类型。在 Scala 里,可以使用复合类型来表达(compound types),复合类型是对象的交集。
假设我们有两个 trait, Cloneable
和 Resetable
:
trait Cloneable extends java.lang.Cloneable {
override def clone(): Cloneable = {
super.clone().asInstanceOf[Cloneable]
}
}
trait Resetable {
def reset: Unit
}
现在假设我们想编写一个接受对象参数的函数 cloneAndReset
,克隆这个对象并重置原对象。
def cloneAndReset(obj: ?): Cloneable = {
val cloned = obj.clone()
obj.reset
cloned
}
问题出现在参数 obj
的类型应该是什么。如果类型是 Cloneable
的,那么对象可以被 clone
,但不能 reset
;如果类型是 Resetable
,那我们可以 reset
对象,但是就没有 clone
操作了。为了避免这种情况下的类型转换,我们可以指定 obj
的类型既是 Cloneable
又是 Resetable
。这样的复合类型在 Scala 中这样写:Cloneable with Resetable
。
下面是更新后的函数:
def cloneAndReset(obj: Cloneable with Resetable): Cloneable = {
//...
}
复合类型可以由多个对象类型组成,它们可以有一个单一的改良(refinement),可用于缩短现有对象成员的签名。一般形式为: A with B with C ... { refinement }
在抽象类型(abstract types) 页面可以查看到如何使用 refinement
的例子。