顯示具有 Swift 標籤的文章。 顯示所有文章
顯示具有 Swift 標籤的文章。 顯示所有文章

2017年11月18日 星期六

Swift -- extension skill, impl policy base design

經過幾個禮拜的思考,我認爲jon hoffman先生說得有理,我們應該傾向簡單的設計,而Swift不是C++,因此不應該試圖模仿
回到Poilcy的設計上,我們最初的期望是:在編譯期選擇型別,並且透過隱式約束,而不是顯式
先看看C++的案例
struct SQLServer {
    void connect();
    // ...
}
struct MySQL {
    void connect();
    // ...
}
class DB<DBImpl> {
public:
    void connect() {
        DBImpl().connect();
    }
}
我們建構泛型DBImpl的實例並且呼叫其方法,我們知道C++的樣板會在"使用"時展開並檢查
因此我們不會有執行期(runtime)成本,且使用時檢查此點非常重要,這是我們之後會在Swift遇到的難題

那麼我們要如何在Swift中實現一樣的能力呢?
首先我們要了解到Swift並非在使用時檢查,而是宣告時檢查,這只得剛才的程式寫法完全無法編譯通過:首先編譯器會告訴你DBImpl沒有建構式(在C++,如果你沒有宣告建構式,編譯器會幫你寫一個),接著編譯器認為DBImpl沒有connect方法
我最開始的解決方案是利用constraint讓編譯器得到這些資訊
protocol DBPolicy {
    init()
    func connect()
}
class SQLServer : DBPolicy {
    required init() { ... }
    func connect() { ... }
    // ...
}
class MySQL : DBPolicy {
    required init() { ... }
    func connect() { ... }
    // ...
}
class DB<DBImpl: DBPolicy> {
    func connect() {
        DBImpl().connect()
    }
}
可是這樣一來,程式就變得沒有彈性,而且這樣搞,寫多型不就好了(我們只享受到一點效能優化,但是沒有得到最重要的彈性)

接下來我學習到where clauses這個特性,我終於找到了正確的實作方式
class SQLServer {
    required init() { ... }
    func connect() { ... }
    // ...
}
class MySQL {
    required init() { ... }
    func connect() { ... }
    // ...
}
class DB<DBImpl> { ... }
extension DB where DBImpl == MySQL {
    func connect() {
        MySQL().connect()
    }
}
這個方案與我們所想像的差不多,確實符合了隱式約束以及編譯期選擇
但是卻有很明顯的重複性,我們有幾個Policy就需要extension幾次,且所做的事都差不多,只能說Swift不是C++,XD

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方法,那麼提供一些實作給常用的型別讓人瞻仰你的厲害,不是啦!是讓別人能夠享受某些成果,那麼這個程式庫方能永恆啊!

Swift --extension概念入門

雖然apple在extension的文件中註記了

NOTE
If you define an extension to add new functionality to an existing type, the new functionality will be available on all existing instances of that type, even if they were created before the extension was defined.
這樣的事實
你仍然不應該在不是定義extension的地方使用該屬性,這樣的分散性會導致未來的維護困難重重

在goto被視作優良設計的年代,我們問「所以究竟是從哪裡來?是執行了a區塊還是b區塊再執行我?」
在callback盛行的時候,我們問「究竟什麼時後執行了callback?」
在共時的世界,我們問「是誰改變了狀態?」

每個設計都是一種抉擇,抉擇之後必然有其優勢與劣勢,問題在於如何善用優勢,而不是造成更多問題

extension啊extension,你比都定義在class好?

讓我們看看傳統的class如何做一個cm => m
class Meter {
  var _v = 0
  init(v: Double) {
    _v = v
  }
  var cm: Double { return _v/100.0 }
}

let onehundredCm = Meter(100).cm
這個實作語意不明,究竟是100公尺轉換成公分,還是公分變公尺?
如果我們想要更加明確的語意,就需要定義大量的輔助類別
例如:
class cm {
    var _v: Double
    var cm: Double { return _v }
    init(v: Double) {
        _v = v
    }
    func toM() -> Meter {
        return Meter(_v/100.0)
    }
}
let onehundredCm = cm(100).toM().m
let oneMeter = Meter(1).toCM().cm
不但複雜,而且可讀性依然有限

那extension呢?(以下做法來自apple developer網站的簡化)
extension Double {
  var m: Double { return self }
  var cm: Double { return self/100.0 }
}
let onehundredCm = 100.0.cm
哇,看起來好多了,對吧!

請考慮
let n = 100.0.cm.cm
// Oops,這是什麼鬼?

extension讓我們能對原生型別進行改動,但也使客戶端需要有更好的意志克制自己亂用的衝動

在effective C++中,Scott Meyer提到,類別的所有介面都應該是透過方法
他的論點如下:
如果你的介面有一個變數,那麼使用者可以對它進行改動,而這個改動可能並非你設計此型別時考慮過的使用順序(這也是C語言的弱點所在,它仰仗使用者知道自己在幹嘛,雖然通常不是那樣),進而導致無解的bug,而這個bug導致使用者不用你的程式庫了!即便這不是你的問題,然而你仍要為此負責
而方法的更多優點是,你可能會驗證傳入的值,再進行對值的改動,而使用方法做為介面者,可以在完全不影響客戶端程式碼的情況下完成新設計,此論點確實值得一試

回到extension
let IWalk = 30.0.km + 20.0.m
extension給了你很好的彈性來設計一門優良的限定領域語言,而這不需要太複雜的技術與知識,只需要簡單的加上幾個屬性

再回到一開始的狀況,事實上,對於該問題,只使用extension或類別都是錯的
混用才能達成我們心目中的效果
例如:
extension Double {
    var cm: CM { return CM(self) }
}
class CM {
    var _v: Double
    func toM() -> Double {
        return Meter(_v/100)._v
    }
    init(v: Double) {
        _v = v
    }
}
使用起來就像
let onehundredCm = 100.cm.toM()
少了一些彈性,但是出現奇怪行為的機會也因此下降,更重要的是盡量不要擴展基本型別,這件事帶來的壞處往往大於好處

那麼extension適合哪些狀況呢?
第一,利用此特性可以減少你需要打的字,而且能夠提升程式碼的可理解性,例如替容器型別定義subscript運算子
第二,限定於此情此景的函數解決方案,例如在一個解析器裡,只有在印出錯誤訊息時,我們才會關心錯誤發生在哪,這時候再定義一個positionFormat函數格式化輸出
第三,型別定義太過冗長,這時在同一個檔案裡用extension替函數分組也是不錯的寫法

好吧,你問為什麼特化解法寫在extension,你就這麼確定其他地方用不上?
當然不是,只是其他地方用上的時候,把extension移到型別定義的檔案中就好
這時就變成了3的情況啦

下一篇會介紹實際運用