2014年10月27日 星期一

關於 Swift 繼承的兩三事

談到繼承就要用先寫一下 Swift 這個語言的 class 的寫法:

class Rectangle {  // 非正確寫法
  var width:Double
  var height:Double
}

這個就是 swift class 的定義方式,名稱為 Rectangle。有兩個 property 各是 width 和 height。 但是這樣寫並不算完成,因為 Swift 要求在產生這個 class 的實體(instance) 的時候所有的 property 都要有明確的值。所以我們來加上初始值

class Rectangle {
  var width:Double = 200.0 
  var height:Double = 300.0
}

這樣簡單的 class 就準備完成,然後我們需要來產生實體如下

var rect = Rectangle()
這樣就可以用 rect 來使用實體,然後改變 property 的內容如下

var rect = Rectangle()
rect.width = 50.0
Rectangle() 做了什麼事?讀者一定很好奇。其實對熟悉 Objective-C 的開發者來說,他就是 alloc - init 這兩個動作的簡化。 在 Swift 裡 Rectangle() 就會對應到 class 裡面的 init()。比如我們改寫一下 Rectangle 成

class Rectangle {
  var width:Double
  var height:Double
  init(){
    width = 200.0
    height = 300.0
  }
}

多了一個 init() 而且 var width 和 var height 之後並沒有給值,這樣可以嗎? 這樣是可行的,因為在真正使用實體前,init() 會最先被呼叫,所以在 init() 裡面把 propety 值都設定好,就可以保證之後存取 property 都不怕沒有值。 讀者可以直接在 playground 上面試試,init() 這個第一個被執行的 method 也有一個別名叫做建構子 - constructor。 然後我們來新增一個 method 名為 area 如下

class Rectangle {
    var width:Double
    var height:Double
    init(){
        width  = 200.0
        height = 300.0
    }
    
    func area() -> Double {
        return width * height
    }
}
var rect = Rectangle()
rect.area() // 會得到 60000 
以上,就完成一個很基本的 Class 的定義。接下來才是繼承的問題 XD 所以我們來產生另一個 class 名為 Square,繼承 Rectangle 如下

class Square : Rectangle {
    
}
var s = Square()
s.height = 20.0
s.width = 20.0
s.area()
Square 裡面空空如也,只有一個 : Rectangle 的宣告,這樣一來 Square 就擁有 Rectangle 所有內容,包含 width 和 height 的 property 還有 area() 這個 method 自然也可以直接使用 這個就是繼承的好處 接下來我們來把 Square 做一些修改,比較符合我們的期待。

class Square : Rectangle { // 非正確寫法
    var side:Double
}

新增一個 side 的 variable 這樣一來要記得在 init() 指定給 side 值。 但是因為 Square 繼承 Rectangle ,已經有一個 init() 所以在 Square 的 init() 前面要加上 override (覆寫) 字樣。如下

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
}
當子類別(指 Square) 要重寫一個和父類別(指 Rectangle)一樣的 method 的時候就要加上 override 的字樣,這樣執行的時候就是採用子類別的 method 程式碼而不是父類別的程式碼。 這樣一來就可以執行如下程式碼

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
        // 最後會執行 super.init()
    }
}
var s = Square()
s.area() // 得到 60000
有沒有發現一個奇怪的地方?按照推理 Square() 會去執行 Square 的 init(),裡面只有 side =50 ,s.area() 是去執行 Rectangle 裡面的 area() 程式碼,因為在 Square 並沒有 override area()。但 init() 裡面沒有對於 width 和 height 有給定值,s.area() 竟然會有值出現。由此我們可以推論 override init() 最後會去執行父類別的 init(),也就把 Square 從 Rectangle 繼承來的 width 和 height 都給了值 但是還是怪怪的,Square 的 area() 竟然不是和 side 有關而是 width 和 height ,所以我們更新 Square 如下

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
    override  func area() -> Double {
        return side * side
    }
}
所以我們覆寫了 area(),讓它和 side 有關。這樣如下的程式碼在 Square 的用法上就更合理

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
    
    override  func area() -> Double {
        return side * side
    }
}
var s = Square()
s.side = 30.0
s.area() // 會得到 900.0
So far, So good 看起來很不錯,但還是有一個小小的缺點。也就是當我們用 Square 的 side 的時候,也可以用 width 和 height,這兩個從 Rectangle 得到的變數,但這三個就是不同的變數,這樣一來觀念上很奇怪。比較好的方式是希望使用 Square 的 width 的時候也就是和 side 有連帶關係。拿 width 來說,如果設定 width 的值同時就是設定 side 的值,取得 width 的值也就是取得 side 的值這樣會比較合理。於是我們要來用到 Swift 語言裡很特別的功能,就是 override property。如下

class Square : Rectangle {
    var side:Double
    override init(){
        side = 50
    }
    
    override  func area() -> Double {
        return side * side
    }
    
    override var width:Double{
        get{
            return side
        }
        set{
            side = newValue
        }
    }
}
覆寫 property var width:Double 的方法如下
  
    override var width:Double{
        get{
            return side
        }
        set{
            side = newValue
        }
    }
前面也要加上一個 override 然後要用 {} 包起來,裡面有 get 和 set 關鍵字。 get 很好理解就回傳 side 的值,set 的區塊裡面要用 newValue 這個保留字代表要設定給 width 的新值,然後指定給 side 相同地 height 也照辦如下
  
override var height:Double{
        get{
            return side
        }
        set{
            side = newValue
        }
}
這樣一來不論是存取 width 或是 height 都是存取 side。 很理想,來測試一下
  
var s = Square()
s.width = 50.0
s.area() // 得到 2500.0
做個這篇的小結

  1. 簡單 class 定義方式, 建構子 init() 使用時機 
  2. 繼承的寫法,init() 在子類別和父類別的使用時機
  3. 覆寫 method 和 property 的方法 
謝謝各位,大家下次見

3 則留言:

  1. 麥克大大你好:

    很感謝你這麼用心的分享,非常的詳細易懂.

    在講到 get set 關年之前的那段繼承,side 被覆寫為50,s.area()是否改回傳為2500

    謝謝!

    回覆刪除
    回覆
    1. 他用的是Double所以没错吧?

      刪除
  2. 大大寫的很詳細,簡單又明瞭,解決了小弟對get set的疑惑。
    太感謝!

    回覆刪除