1. 程式人生 > >Scala 覆寫類成員和trait成員

Scala 覆寫類成員和trait成員

Scala 覆寫類成員和trait成員

我們可以在類中及trait 中宣告抽象成員,包括抽象欄位、抽象方法和抽象型別。在建立例項前,繼承類或trait 必須定義這些抽象成員。大多數面嚮物件語言都支援抽象方法,其中某些語言還支援抽象欄位和抽象型別。
假如你需要對Scala 的某一具體成員進行覆寫,覆寫該成員時必須使用override 關鍵字。假如某一子型別定義(也可以說“覆寫”)了抽象成員,override 關鍵字是可省略的。反過來說,如果並未覆寫某一成員但卻使用了override 關鍵字,這會導致錯誤。

強制使用override 關鍵字會帶來下列好處。
• 有些成員本應對其他成員進行覆寫,而使用override 關鍵字能夠捕獲這些成員的拼寫錯誤。假如這些成員未覆寫任何成員,編譯器便會丟擲錯誤。
• 向基類中新增新的成員時,由於基類開發人員對繼承類並不太瞭解,這就可能會出現新添的成員名與繼承類中某一已經存在的成員名稱衝突,而Scala 能夠捕獲這一細微的錯誤。也就是說,我們不希望繼承類中的成員對基類成員進行覆寫,而由於該繼承類成員未提供override 關鍵字,當引入這個新的基類成員時,編譯器便會丟擲錯誤。
• 由於必須新增這一關鍵字,這有助於提醒你考慮到底哪些成員應該被覆寫。

Java 提供了@Override 對方法進行註解,你可以選擇是否新增該註解。儘管該註解能夠輔助使用者捕獲拼寫錯誤,但是由於註解是可選的,因此它無法幫助使用者捕獲上面第二項所描述的細微錯誤。
在實現抽象方法時,能否選擇性地使用override 關鍵字呢?我們一起聽聽贊同和反對的聲音。
除了之前提出的幾點之外,贊同使用override 關鍵字的理由還包括如下2 點。
• 使用override 關鍵字能提醒讀者,定義在父類中的某一成員已經被實現了(也有可能被覆寫了)。
• 假如父類移除了已經在某一子類中定義了的抽象成員,系統將會報錯。
而反對使用override 關鍵字的理由如下。
• 捕獲拼寫錯誤其實並沒必要。未定義“覆寫”也意味著當前仍然存在未定義的成員,因此繼承類(或該類的其他具體類)無法通過編譯。
• 在程式碼庫的演變過程中,假如維護父類某一抽象成員的開發人員決定將該抽象成員改為具體成員,這一變化在編譯子類時無法被注意到。現在子類應該呼叫該方法的父類實現嗎?編譯器會默默地使用子類方法覆寫父類新定義的實現。

避免覆寫具體成員

換句話說,抽象成員到底應不應該使用override 關鍵字呢?我們很難給出答案。這一問題也引入了更多的問題:你是否應該覆寫一個具體方法呢?正確的回答是,大多數情況下,你不應該這樣做。
父型別和子型別的關係就好比一紙契約,我們需要費心確保子類沒有破壞父型別所指定的實現行為。
覆寫具體成員時,很容易破壞掉這紙契約。覆寫foo 方法時是否應該呼叫super.foo 方法呢?如果呼叫了super.foo 方法,子類的實現方法應該什麼時候呼叫該方法呢?當然,正確的做法取決於具體的場景。
著名的“四人組”所編寫的《設計模式》一書中描述的模版方法模式(template methodpattern)構造了一個更為牢固的契約。在該模式中,父類提供了某一方法的具體實現,並以此定義了某一行為的主要輪廓。而需要使用多型行為時,該方法也會呼叫一些protected 抽象方法。在此之後,子型別則只需要實現proteced 抽象方法即可。
下面我們給出這樣一個示例,說明供美國公司使用的工資計算器的粗略實現。

case class Address(city: String, state: String, zip: String)

case class Employee(name: String, salary: Double, address: Address)

abstract class Payroll {
  def netPay(employee: Employee): Double = { // netPay 方法應用了模版方法模式。該方法定義了計算工資的協議,並將像這類每年都會變化的具體細節委託給抽象方法處理。
    val fedTaxes = calcFedTaxes(employee.salary)
    val stateTaxes = calcStateTaxes(employee.salary, employee.address)
    employee.salary - fedTaxes -stateTaxes
  }
  def calcFedTaxes(salary: Double): Double //計算美國聯邦稅。
  def calcStateTaxes(salary: Double, address: Address): Double //計算州稅
}
object Payroll2014 extends Payroll {
  val stateRate = Map(
    "XX" -> 0.05,
    "YY" -> 0.03,
    "ZZ" -> 0.0)
  def calcFedTaxes(salary: Double): Double = salary * 0.25 // 定義父類中抽象方法的具體實現
  def calcStateTaxes(salary: Double, address: Address): Double = {
    // Assume the address.state is valid; it's found in the map!
    salary * stateRate(address.state)
  }
}

請注意,本實現中未出現override 關鍵字。
對於“不要覆寫父類具體方法”這條規則,我能想到兩個例外。某一方法的父類實現對於子類而言確實沒有用處,這是其一。例如toString、equals 和hashCode 方法。不幸的是,覆寫這些無用的預設方法非常普遍,以至於我們都滿足於對具體方法進行覆寫。
除非具體方法是toString 這樣的無用方法,否則請不要覆寫具體方法。除非你確實是在覆寫具體方法,否則不要使用override 關鍵字。

假如某一宣告中包含final 關鍵字,那麼Scala 不允許覆寫該宣告。