初始抽象 val 成員


抽象成員 中最後一個例子,如果在父類別(特徵)中是抽象val成員,在子類別實作時,如果執行順序與你的應用程式行為有關時,需注意一下執行的順序問題。

如果是抽象類別的話,可以藉由主要建構式來確保執行的順序:
abstract class Circle(r: Double) {
val radius = r
val area = Math.Pi * radius * radius
}

def calRadius = 10.0

class RedCircle(r: Double) extends Circle(r)

val c = new RedCircle(calRadius)
println(c.area) // 314.1592653589793

在建構RedCircle時,將calRadius函式的傳回值給RedCircle建構時使用,所以會先執行calRadius取得值,再來進行的是父類別的建構式,將r指定給radius,並將圓面積計算結果指定給area。

如果是匿名類別的方式實作的話,只要透過主要建構式傳入參數,則執行的流程是相同的:
abstract class Circle(r: Double) {
val radius = r
val area = Math.Pi * radius * radius
}

def calRadius = 10.0

val c = new Circle(calRadius) {
// 其它實作
}

println(c.area) // 314.1592653589793

但是特徵不能定義主要建構式,所以你可能會如下撰寫程式:
trait Circle {
val radius: Double
val area = Math.Pi * radius * radius
}

def calRadius = 10.0

class RedCircle(r: Double) extends Circle {
val radius = r
}

val c = new RedCircle(calRadius)
println(c.area) // 314.1592653589793

使用建構式來確保執行流程是比較好的。但如果因為某個因素,你要使用匿名類別時:
trait Circle {
val radius: Double
val area = Math.Pi * radius * radius
}

def calRadius = 10.0

val c = new Circle {
val radius = calRadius
}

println(c.area), // 0.0

上 例最後顯示的結果是0.0。你使用匿名類別的方式,實作Circle特徵並建立實例,建立實例時的執行順序是,先執行Circle內容的初始化區塊,此時 radius被初始為0.0,而area計算的結果也就是0.0,接下來執行calRadius函式,將結果設定給radius。所以最後你使用 c.area取得的結果會是0.0。

解決以上執行順序問題的方式之一,是使用早期物件初始區段(Early object initialization section)來預先初始抽象val成員,例如:
trait Circle {
val radius: Double
val area = Math.Pi * radius * radius
}

def calRadius = 10.0

val c = new { val radius = calRadius } with Circle {
// 其它定義
}

println(c.area) // 314.1592653589793

早期物件初始區段中只允許定義val成員的實作。執行的順序是,先執行早期物件初始區段,此時radius被設定為calRadius的執行結果,接著再執行Circle特徵中的程式碼,所以area根據radius的結果計算出來,然後才是匿名類別的建構式區塊。

基本上,定義具名類別時,也可以使用早期物件初始區段:
trait Circle {
val radius: Double
println(radius)
val area = Math.Pi * radius * radius
}

def calRadius = 10.0

class RedCircle(r: Double) extends {
val radius = r
} with Circle {
// 其它實作
}

val c = new RedCircle(calRadius)
println(c.area) // 314.1592653589793

另一個解決的方式,則是延遲具體val成員的運算,例如:
trait Circle {
val radius: Double
lazy val area = Math.Pi * radius * radius
}

def calRadius = 10.0

val c = new Circle {
val radius = calRadius
}

println(c.area) // 314.1592653589793

在Circle中定義具體的val成員area時,你加上了lazy修飾,這表示在真正需要呼叫到area前,不會真正去計算出area的值。所以上例使用匿名類別的方式,實作Circle特徵並建立實例,建立實例時的執行順序是,先執行Circle內容的初始化區塊,此時radius被 初始為0.0,而area計算被延遲,接下來執行calRadius函式,將結果設定給radius(也就是10.0)。最後你使用c.area要取得的結果 時,才真正計算area的值,所以此時使用到radius是10.0,所以area運算出314.159....,最後顯示結果。

使用lazy初始val成員時,初始的過程最好別產生邊際效應(Side effect),也就是lazy val成員初始時,不會改變物件的其它狀況,否則你就得費心地處理lazy val初始前後物件的差異性對應用程式所造成的影響。