2012-01-29 60 views
7

從Odersky的書編寫一個簡單的例子,導致以下問題:Scala的繼承問題:VAL與DEF

// AbstractElement.scala 
abstract class AbstractElement { 
    val contents: Array[String] 
    val height: Int = contents.length // line 3 
} 

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { // line 6 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} 

object AbstractElement { 
    def create(ch: Char): AbstractElement = { 
    new UnifiedElement(ch, 1, 1) // line 12 
    } 
} 

// ElementApp.scala 
import AbstractElement.create 

object ElementApp { 

    def main(args: Array[String]): Unit = { 
    val e1 = create(' ') // line 6 
    println(e1.height) 
    } 
} 

編譯器會引發以下跟蹤:

Exception in thread "main" java.lang.NullPointerException 
    at AbstractElement.<init>(AbstractElement.scala:3) 
    at UnifiedElement.<init>(AbstractElement.scala:6) 
    at AbstractElement$.create(AbstractElement.scala:12) 
    at ElementApp$.main(ElementApp.scala:6) 
    at ElementApp.main(ElementApp.scala) 

所以編譯器認爲內容仍然是空的,但我在UnifiedContainer中定義了它!

當我用def和evrth替換val完美時,事情變得更加奇怪!

您能否請您認真考慮這種行爲?

回答

13

Here是Paul P的一篇很好的文章,它解釋了Scala中的初始化順序錯綜複雜。作爲一個經驗法則,您應該使用從不使用摘要val s。始終使用摘要def s和lazy val s。

+2

你能否澄清'總是使用抽象'def's和'懶惰val's?編譯器不允許你在抽象類中放置'lazy val'。 – jbx 2013-11-17 01:03:59

+1

@jbx,是的,你是對的。那隻適用於特質。 – missingfaktor 2013-11-17 05:22:14

+0

我看到的一些例子在抽象類中使用'val',然後在擴展它的具體類中使用'lazy val'。這是正確的方法嗎? (我仍然在學Scala,所以感到困惑一點) – jbx 2013-11-17 18:54:50

4

AbstractElement的定義中,您實際上正在定義一個構造函數,它將內容初始化爲null並計算contents.length。 UnifiedElement的構造函數調用AbstractElement的構造函數,然後才初始化內容。換句話說,我們有一個已經存在於Java(和任何OOP語言)中的問題的新實例:超類的構造函數調用在子類中實現的方法,但後者不能安全因爲該子類尚未構建而被調用。抽象的vals只是觸發它的方法之一。

這裏最簡單的解決方案就是製作height a def,這是更好的方法,並且要注意其他答案中鏈接的初始化規則。

abstract class AbstractElement { 
    val contents: Array[String] 
    def height: Int = contents.length //Make this a def 
} 

稍微更復雜的解決方案,而不是被強迫contents前高度,你可以用這種語法做初始化

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends { 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} with AbstractElement { 
    //... 
} 

注意混入組成,即with ,不是對稱的 - 它從左到右工作。並且注意,{}最後可以省略,如果你沒有定義其他成員。

懶惰vals也是一個解決方案,但它們會產生相當的運行時間開銷 - 無論何時讀取變量,生成的代碼都會讀取一個易失性位圖來檢查該字段是否已經初始化。

製作contents a def這裏似乎是一個壞主意,因爲它會經常重新計算。

最後,避免抽象的vals是恕我直言,一個極端的措施。有時他們是正確的 - 你應該小心具體的vals,指的是抽象的vals。

編輯:看起來,而不是一個抽象的val,可以使用抽象定義並用一個具體的val覆蓋它。這確實是可能的,但如果有具體的vals指的是抽象的定義,這並沒有幫助。考慮上述代碼的這個變體,並注意成員是如何定義的:

abstract class AbstractElement { 
    def contents: Array[String] 
    val height: Int = contents.length // line 3 
} 

class UnifiedElement(ch: Char, _width: Int, _height: Int) extends AbstractElement { 
    val contents = Array.fill(_height)(ch.toString() * _width) 
} 

該代碼具有相同的運行時行爲由OP給出的代碼,即使AbstractElement.contents現在是一個def:身體訪問者讀取一個只由子類構造函數初始化的字段。抽象值和抽象定義之間的唯一區別似乎是抽象值只能被具體值覆蓋,所以如果這是你想要的,限制子類的行爲可能是有用的。

+0

你能否解釋一下'val'有時是正確的東西?它是否提供了比「抽象」 def'被一個具體的'val'方法覆蓋了嗎? – missingfaktor 2012-01-30 03:41:14

+0

我想過了,但是它並不能解決問題,正如我現在解釋的那樣。 – Blaisorblade 2012-01-31 09:21:37