Scala 路徑相依型態(Path-dependent type)

類別中可以有類別,基本上稱之為內部類別(Inner class),例如:
class Outer {
private val x = 10
class Inner {
private val y = x + 10
}
}

內部類別可以存取外部類別的成員(包括private成員),而外部類別無法存取內部類別的private成員,如果你想要外部類別可以存取內部類別的private成員,可以參考
存取修飾 的介紹

內部類別之所以能存取外部類別的成員,是因為內部類別會隱含地參考至外部類別所建構的實例,也因此如果你要使用內部類別來建構實例時,必須先建立外部類別物件(如此才有外部類別實例可以參考)。一個例子如下所示:
val outer = new Outer
val inner = new outer.Inner

那麼內部類別的型態是什麼?又或者問inner參考名稱的型態是什麼?答案是outer.Inner!你確實是可以使用outer.Inner來宣告型態的:
val outer = new Outer
val inner: outer.Inner = new outer.Inner

outer.Inner這樣的型態,稱之為路徑相依型態(Path-dependent type),所謂路徑(Path),指的是參考住外部類別所建立實例的名稱,就outer.Inner這個型態來說,其路徑為outer。之所以稱之為路徑相依型態,是因為不同的路徑,就代表著不同的型態。例如:
val o1 = new Outer
val o2 = new Outer
val i1 = new o1.Inner
val i2 = new o2.Inner

在上例中,o1.Inner與o2.Inner是不同的型態,i1的型態是o1.Inner,i2的型態是o2.Inner。所以下例中,不能通過編譯:
val o1 = new Outer
val o2 = new Outer
val i: o2.Inner = new o1.Inner // 編譯錯誤,type mismatch

注意!相依的是路徑的名稱,不是路徑所參考的物件。例如:
val o1 = new Outer
val o2 = o1
val i1: o1.Inner = new o1.Inner
val i2: o2.Inner = new o1.Inner // 編譯錯誤,type mismatch

在上例中,o2與o2參考的雖然是同一物件,然而o1.Inner與o2.Inner依然是不同的型態。事實上,o1.Inner、o2.Inner等型態,都是一種Outer#Inner,也就是Outer#Inner的子類型:
val o1 = new Outer
val o2 = new Outer
val i1: Outer#Inner = new o1.Inner
val i2: Outer#Inner = new o2.Inner

你要拿路徑相依型態來繼承也是可行的:
val o = new Outer
class Some extends o.Inner
val oi: Outer#Inner = new Some

再來看看
型態(type)成員 中的一個例子:
class Food

class Fish extends Food {
override def toString = "魚"
}

abstract class Animal {
type F <: Food
def eat(f: F)
}

class Cat extends Animal {
type F = Fish
def eat(fish: Fish) {
println("吃" + fish)
}
}

val cat1 = new Cat
val cat2 = new Cat
cat1.eat(new cat1.F) // 吃魚
cat2.eat(new cat2.F) // 吃魚
cat1.eat(new cat2.F) // 吃魚
cat2.eat(new cat1.F) // 吃魚

對於Cat而言,F是其類別成員之一,實際上cat1.F與cat2.F代表著兩個不同的別名,這也是路徑相依的一個例子,只不過在上例中,cat1.F與cat2.F這兩個名稱,剛好都是被指定為Fish的別名,所以上例中最後兩行執行是沒有問題的,就像是以下的例子:
type F1 = Fish
type F2 = Fish
cat1.eat(new F1) // 吃魚
cat1.eat(new F2) // 吃魚

實際上,無論是別名cat1.F或是別名cat2.F,都是Cat#F的特例:
val cat = new Cat
cat.eat(new Cat#F) // 吃魚
對於路徑相依型態,不同的路徑代表不同的型態,所以你可以將之作為快速建立特定類型的一種方式,只要指定不同的路徑,就可以馬上產生不同的型態。一個應用的實際例子是在製作列舉型態(Enumation type),例如:
class Enum {
class Value
}

object Action extends Enum {
val Up = new Value
val Down = new Value
val Left = new Value
val Right = new Value
}

object Operation extends Enum {
val Up = new Value
val Down = new Value
val Left = new Value
val Right = new Value
}

val action1: Action.Value = Action.Up
val action2: Action.Value = Operation.Up // 編譯錯誤,type mismatch

在上例中,Value為Enum的內部類別,由於路徑相依的關係,Action的Up、Down、Left、Right成員,其型態為Action.Value(別忘了,Action實際上是個參考名稱,請參考
單 例物件),而Operation的Up、Down、Left、Right成員,其型態為Operation.Value(Action.Value與Operation.Value都是一種Enum#Value),利用這種方式,可讓建立出來的列舉成員因路徑而有不同的型態。

所以,在以下的例子中,doAction()可以傳入Action.Value的列舉物件,但無法傳入Operation.Value的列舉物件:
def doAction(action: Action.Value) = action match {
case Action.Up => "上"
case Action.Down => "下"
case Action.Left => "左"
case Action.Right => "右"
}

println(doAction(Action.Down))
println(doAction(Operation.Down)) // 編譯錯誤,type mismatch

事實上,Scala本身就提供了 scala.Enumeration 類別用以建立列舉型態:
object Action extends Enumeration {
val Up = Value
val Down = Value
val Left = Value
val Right = Value
}

scala.Emuneration中有個內部抽象類別Value,以及一個無參數方法Value,無參數方法Value會傳回
內部抽象類別Value的實作物件(內部Val類別實作了內部抽象類別Value)。上例也可以寫成:
object Action extends Enumeration {
val Up, Down, Left, Right = Value
}

scala.Emuneration中的Value方法還有幾個版本,像是接受String的版本可以讓你指定列舉物件的toString結果:
object Action extends Enumeration {
val Up = Value("上")
val Down = Value("下")
val Left = Value("左")
val Right = Value("右")
}

for(value <- Action) {
println(value.id + ": " + value)
}

在上例中也示範了,你可以使用foreach語法取得Action中的列舉物件,如果沒有指定列舉常數值,則預設從0開始,這可以從id屬性取得,上例會顯示:
0: 上
1: 下
2: 左
3: 右


scala.Emuneration中接受整數的Value方法,則可以讓你指定列舉常數值,而另一個同時接受整數與字串的版本則可以讓你同時指定列舉常數與列舉物件的字串描述。例如:
object Action extends Enumeration {
val Up = Value(10, "上")
val Down = Value(20, "下")
val Left = Value(30, "左")
val Right = Value(40, "右")
}

for(value <- Action) {
println(value.id + ": " + value)
}

上例會顯示
10: 上
20: 下
30: 左
40: 右


scala.Emuneration也定義了apply()方法,所以你可以使用()指定列舉常數值來取得對應的列舉物件,例如:
println(Action(10))   // 上



迴響:

發表迴響:
  • HTML 語法: 關閉

Search







follow caterpillar at http://twitter.com


Feeds

Referers

Navigation