Scala之旅-內部類(INNER CLASSES)和複合型別(COMPOUND TYPES)
內部類(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
的例子。