2017年9月29日 星期五

Extension hack

好吧,上一篇說了這麼多,其實幾乎就只是把屬性定義在類別外罷了,沒什麼啊
這樣並沒有比class強到哪裡

所以,讓我們來看看extension hacks吧!
hack 1:
extension from a temporary protocol
protocol MenuItem {}
extension label : MenuItem {}
extension button : MenuItem {}

var list = [MenuItem]()
list.append(label("Hello"))
list.append(button("Click me", event))
哇,這下我們可以用List<MenuItem>來存放我們所想要存放的型別了,只要將你想要存放的型別extension一下MenuItem這個協定,就是這麼簡單

注意這個機制有幾個問題,第一是如果你想要呼叫某個屬於label的方法,你將會得到沒有此方法的編譯期錯誤

那,有辦法解決嗎?
有!首先我們要知道為什麼會這樣,如果你曾經在shell中嘗試過印出type(of: xxx)
那麼你一定知道型別後面都接有一個位元組,這個位元組,其實就是實際上型別在執行期的樣子啦!因此在執行期中,為了確保最大的安全度,編譯器常用最小介面原則,選擇概念最寬廣的那個型別
那麼編譯器要怎麼知道你是要呼叫子型別的方法還是父型別的呢?
在C++我們可以用->運算子以及指標,確保我們直接存取實體,而且我們不需要聲明子型別是什麼,因為編譯器有在記
不過產生的問題就是,有時候你並不知道到底有沒有這個方法(當然,新的IDE與工具們提供了這些,但是我們常常還是編譯下去之後才知道),進而需要搜尋你用了哪個子型別
而在Swift,我們用(instance as! Type).method()使用子型別的方法,缺點是那個括號跟不甚明瞭的語意,而且我們為了確保安全,還要多做一個if instacne is Type的檢查

回到Swift
第一種作法是直接在MenuItem上定義一個方法作為統一的介面,任何型別擴展MenuItem時,就實作該方法

第二種作法是我們將MenuItem轉換成原本的型別
這種作法有個小問題:
問題在於,我們知道label是一種MenuItem,但是你怎麼知道,某個MenuItem是label?
所以我們需要對它進行危險的轉換

 MenuItem() as! label // as! 意思是將左邊的值當成右邊的型別來使用,而且這是危險的
ps. 這只是示意,不能運作
而這對工作上非常不合用,也很難凸顯我們想要做什麼
所以我寫了一個轉換函式
func convert<F, T>(from: F, to: T) -> T {
    if from is T {
        return from as! T
    } else {
        return to
    }
}
ps. 請不要真的用這個函數做事,這只是為了先避開複雜議題(例外處理)才這樣寫的
於是我們可以用

let res = convert(from: MenuItem(), to: label())
取得一個轉換結果,在這裡因為我們失敗時(from不是一種T)就回傳to
我們沒辦法知道是成功抑或失敗,因此我們應該對此有所區別
func downCast<F, T>(from: F, to: T) -> T? {
    if from is T {
        return from as? T
    } else {
       return nil
    }
}
這是第二版的轉換函數,我用downCast是要說明我們在做危險的向下轉型(上面的convert則是說明它是通用的轉換)
同時這次失敗將回傳nil
因此使用上使用者將需要多負擔一個!來解包
let res = downCast(from: num, to: Double())
print(res!)
藉由nil,這個版本保證我們通常能知道有沒有轉型失敗(不過回傳nil雖然侵入性小,卻也把檢查責任丟給客戶端,而且不能應付本來就是nil的實體)

同時,我認為大部分時候,我們不應該用第一種作法,除非你真的很確定你只是需要這個方法
為什麼說第二種作法比較好呢?因為我們經常性面對的問題通常與App開發有關
因此需要確切型別的機會比較高,而且第二種作法的侵入性低,未來要對介面進行改變也比較容易,同時Swift可還有傳統的介面繼承啊!如果真的需要某個方法提供行為,應該用繼承的方式,直接定義在class宣告上

在呼叫轉換函數時,可以看到
convert(from: xxx, to: label())
to接收一個實體,我稱之為Target Type instance,只要忽略它的括號,我們就能取得還不錯的可讀性,可喜可賀可喜可賀!

hack 2:
default subset of protocol
如果我們想要做一個新的協定,同時不希望使用者還要浪費時間定義哪些可以符合協定
我們可以利用extension
protocol Format {}
extension Double : Format {}
extension Int : Format {}
這個hack跟上一個hack有87%像,讓他們有差別的地方在於所求不一樣
hack 2專注於提供一組符合協定的預設型別集合
相較於hack 2,hack 1只在乎如何讓自訂的型別放進一個泛型容器之中,以及我們怎麼安全的拿出來
hack 2的重點是讓某個你提供的protocol具有已經具現化的可使用型別集合
所以我這裡舉了Format作為例子,假設你提供了一個Format protocol給你的Logger函式庫,Format protocol要求使用者實作format方法,那麼提供一些實作給常用的型別讓人瞻仰你的厲害,不是啦!是讓別人能夠享受某些成果,那麼這個程式庫方能永恆啊!

沒有留言:

張貼留言