Swift 命名空間形式擴展的實現
Swift 的 extension 機制很強大,不僅可以針對自定義的類型,還能作用於系統庫的類型,甚至基礎類型比如 Int。當在對系統庫做 extension 的時候,就會涉及到一個命名沖突的問題。Objective-C 時代的通行解決辦法是在擴展方法名字的最前面加上 XXX_ 形式的前綴。這種形式不但解決了命名沖突的問題,而且增強了代碼可讀性。一旦閱讀到這種風格的方法名,就知道是非系統的實現。Swift 社區最初的一段時間內,也是按照這種命名方式來做的。
Swifty
在前綴形式的擴展使用了一段時間之後,大家漸漸覺得前綴形式的 Objective-C 風格不再適合 Swift。在命名方式上,社區掀起了一股 Swifty 化的風潮。WWDC 2016 的 Session 403 Swift API Design Guidelines 中詳細闡述了 Swifty 風格的命名規則,而 Swift 3 的大量 API 的改動,也是按照這種風格進行的演進。很多開源的 Swift 陸陸續續在接下來的版本中,拋棄了之前的前綴命名形式,改用了 namespace 形式的命名。比如 RxSwift 的 rx_ => rx.,SnapKit 的 snp_ => snp.
對我們來說,實際的開發過程中,也經常會對系統庫中的已有類型做自定義的擴展,如果有一種通用的形式,來實現這種擴展,那就太好了。
原理
namespace 形式擴展的原理,就是對原類型進行一層封裝。在 Swift 中,這個封裝類型使用的是 Struct,然後,對這個 Struct 進行自定義的方法擴展。
代碼實現
以我剛寫的一個開源庫 HandOfTheKing 舉例,這個庫的名字起源於冰與火之歌中的’國王之手’,主要功能是提供一個基於 RxSwift 和 SnapKit 的 iOS App 開發環境,並包含一些 iOS 開發中的實用擴展,比如鏈式的 UI 布局代碼實現。其中的 HandOfTheKing/Namespace.swift 文件裏,包含了命名空間形式擴展的實現。源碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public protocol NamespaceWrappable {
associatedtype WrapperType
var hk: WrapperType { get }
static var hk: WrapperType.Type { get }
}
public extension NamespaceWrappable {
var hk: NamespaceWrapper<self> {
return NamespaceWrapper(value: self)
}
static var hk: NamespaceWrapper<self>.Type {
return NamespaceWrapper.self
}
}
public protocol TypeWrapperProtocol {
associatedtype WrappedType
var wrappedValue: WrappedType { get }
init(value: WrappedType)
}
public struct NamespaceWrapper<t>: TypeWrapperProtocol {
public let wrappedValue: T
public init(value: T) {
self.wrappedValue = value
}
}</t></self></self>
|
使用方式
如果只想使用的話,把上面的源碼拖到你的工程裏,然後修改 hk 為任何你想要的前綴即可。擴展使用方式也很簡單,舉一個實際使用情況的例子,如果想要對 String 擴展一個名為 test 的方法,方法返回自身內容。除了寫成方法,為了更便於使用,定義成計算屬性也是可以的,代碼如下:
1 2 3 4 5 6 |
extension String : NamespaceWrappable { }
extension TypeWrapperProtocol where WrappedType == String {
var test: String {
return wrappedValue
}
}
|
然後就可以按照下面的形式來使用這個擴展
1 2 |
let testStr = "foo" .hk.test
print(testStr)
|
在對 TypeWrapperProtocol 這個協議做 extension 時, where 後面的 WrappedType 約束可以使用 == 或者 :,兩者是有區別的。如果擴展的是值類型,比如 String,Date 等,就必須使用 ==,如果擴展的是類,則兩者都可以使用,區別是如果使用 == 來約束,則擴展方法只對本類生效,子類無法使用。如果想要在子類也使用擴展方法,則使用 : 來約束。
還有一些註意的地方
-
由於 namespace 相當於將原來的值做了封裝,所以如果在寫擴展方法時需要用到原來的值,就不能再使用 self,而應該使用 wrappedValue。
-
對類型擴展實現 NamespaceWrappable 協議,只需要寫一次。如果對 UIView 已經寫了 NamespaceWrappable 協議實現,則 UILabel 不需要再寫。實際上寫了之後,編譯會報錯。
-
如果在實現的 func 前加上 static 關鍵字,可以擴展出靜態方法。
代碼分析
解釋一下實現的代碼,由於使用了 protocol 和 generic 來實現,這裏的代碼不是很容易理解。
首先是定義了一個 NamespaceWrappable 協議,這個協議代表了支持 namespace 形式的擴展。並緊接著給這個協議 extension 了默認實現。這樣實現了這個協議的類型就不需要自行實現協議所約定的內容了。
NamespaceWrappable 協議的默認實現返回了 NamespaceWrapper 這個帶有泛型的 Struct。同時這個 Struct 實現了 TypeWrapperProtocol 協議。而 TypeWrapperProtocol 協議也帶有泛型,而這兩個泛型相互關聯。這樣就形成了最終的寫法。
如果沒有 TypeWrapperProtocol 這個協議,是可以直接對 NamespaceWrapper 這個泛型的 Struct 進行擴展的,對 Objective-C 的類來說這樣的寫法沒有問題,但當嘗試對 Swift 的值類型進行擴展時,會產生編譯錯誤,比如下面這兩種寫法的代碼:
1 2 3 4 |
extension NamespaceWrapper where T == String {
}
extension NamespaceWrapper where T: String {
}
|
會產生不同的編譯錯誤,有興趣可以嘗試。為了統一,則多封裝了 TypeWrapperProtocol 這個協議。
http://www.cocoachina.com/ios/20171031/21000.html
Swift 命名空間形式擴展的實現