泛型編碼的目的
表達(dá)算法或者數(shù)據(jù)結(jié)構(gòu)所要求的核心接口捐名。(核心接口是什么呢?也就是找到想要實(shí)現(xiàn)的功能的最小需求。)
泛型編碼帶來(lái)的優(yōu)勢(shì):
1田晚、可以寫出可重用的函數(shù)和數(shù)據(jù)結(jié)構(gòu),比如說(shuō)Array,Set……
2国葬、可以創(chuàng)建泛型方法贤徒,比如說(shuō)func identity< A > (input: A) -> A
今天的研究?jī)?nèi)容:
1、如何書寫泛型代碼
2胃惜、談一談編譯器是如何處理泛型代碼
3泞莉、如何優(yōu)化我們的泛型代碼
重載
-
什么是重載?
重載就是擁有同樣的名字船殉,但是參數(shù)或者返回類型不同的多個(gè)方法互相稱為重載方法鲫趁。
func log(_ view: UIView) { print("It's a \(type(of: view)), frame: \(view.frame)")
}
func log(_ view:UILabel){
let text = view.text ?? "empty"
print("It's a label,text:(text)")
}
- 那么問(wèn)題來(lái)了swift是怎么來(lái)確定到底使用哪個(gè)重載函數(shù)呢?
選擇最具體的一個(gè)利虫,也就是說(shuō)非通用的函數(shù)會(huì)優(yōu)先于通用函數(shù)挨厚。
```
let label = UILabel(frame: CGRect(x: 20, y: 20, width: 200, height: 32))
label.text = "password"
log(label)//It's a label,text:password
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
log(button)//It's a UIButton, frame: (0.0, 0.0, 100.0, 50.0)
-
需要注意
1、重載的使用是在編譯期間靜態(tài)決定的糠惫,編譯器會(huì)依據(jù)變量的靜態(tài)類型來(lái)決定調(diào)用那一個(gè)重載疫剃,而不是運(yùn)行時(shí)根據(jù)值的動(dòng)態(tài)類型來(lái)決定的。
2硼讽、但是當(dāng)使用操作符重載時(shí)巢价,編譯器會(huì)表現(xiàn)出一些奇怪的行為:類型檢查器會(huì)去使用非泛型的版本寄猩,而不考慮泛型版本癌别。
precedencegroup ExponentiationPrecedence { associativity:left higherThan:MultiplicationPrecedence } infix operator **: ExponentiationPrecedence func **(lhs: Double, rhs: Double) -> Double { return pow(lhs, rhs) } func **(lhs: Float, rhs: Float) -> Float { return powf(lhs, rhs) }
//加一個(gè)對(duì)整數(shù)的重載
func **<I: SignedInteger>(lhs: I, rhs: I) -> I {
// 轉(zhuǎn)換為 IntMax鹅经,使用 Double 的重載計(jì)算結(jié)果的畴,
// 然后用 numericCast 轉(zhuǎn)回原類型
let result = Double(lhs.toIntMax()) ** Double(rhs.toIntMax())
return numericCast(IntMax(result))
}
func **<I: UnsignedInteger>(lhs: I, rhs: I) -> I {
let result = Double(lhs.toIntMax()) ** Double(rhs.toIntMax())
return numericCast(IntMax(result))
}
如果我們就直接執(zhí)行2**3状答,會(huì)報(bào)錯(cuò):
Playground execution failed: error: 泛型.playground:75:2: error: ambiguous use of operator ''
23
^
泛型.playground:58:6: note: found this candidate
func **(lhs: Double, rhs: Double) -> Double {
^
泛型.playground:61:6: note: found this candidate
func **(lhs: Float, rhs: Float) -> Float {
^
因?yàn)槭褂貌僮鞣剌d的時(shí)候辅鲸,編譯器會(huì)使用非泛型的版本菩彬,2和3自動(dòng)向上轉(zhuǎn)換為Double 或者Float赁还,由于兩者對(duì)于整數(shù)字面量來(lái)說(shuō)是相同的優(yōu)先級(jí)可選項(xiàng)并齐,所以說(shuō)編譯器無(wú)法確定去調(diào)用Double的重載還是Float的重載漏麦。
解決方法:
1、至少將一個(gè)參數(shù)顯示地聲明為整數(shù)類型(let intResult = Int(2)**3)
2况褪、明確提供返回值的類型(let intResult:Int = 2**3)
####使用泛型約束進(jìn)行重載
略寫了
####使用泛型進(jìn)行代碼設(shè)計(jì)
泛型在我們進(jìn)行程序設(shè)計(jì)的時(shí)會(huì)非常有用撕贞,它能夠幫助我們提取共通的功能,并且減少模版代碼窝剖。
- 讓我們來(lái)寫一些與網(wǎng)絡(luò)服務(wù)交互的函數(shù):
獲取用戶列表的數(shù)據(jù),并將它解析為User數(shù)據(jù)模型(為了簡(jiǎn)化??麻掸,使用的是同步方式,在實(shí)際開(kāi)發(fā)中赐纱,應(yīng)當(dāng)使用異步方法加載數(shù)據(jù))
```
//最原始的方式來(lái)實(shí)現(xiàn)
func loadUsers(callback: ([User]?) -> ()) {
let usersURL = webserviceURL.appendingPathComponent("/users")
let data = try? Data(contentsOf: usersURL)
let json = data.flatMap {
try? JSONSerialization.jsonObject(with: $0, options: [])
}
let users = (json as? [Any]).flatMap { jsonObject in
jsonObject.flatMap(User.init)
}
callback(users)
}
```
如果我們想要寫一個(gè)相同的函數(shù)來(lái)加載其它資源脊奋,比如說(shuō)加載博客文章的函數(shù):
```
//最原始的方式來(lái)實(shí)現(xiàn)
func loadBlogPosts(callback: ([BlogPost]?) -> ()) {
let blogpostURL = webserviceURL.appendingPathComponent("/blogposts")
let data = try? Data(contentsOf: blogpostURL)
let json = data.flatMap {
try? JSONSerialization.jsonObject(with: $0, options: [])
}
let blogposts = (json as? [Any]).flatMap { jsonObject in
jsonObject.flatMap(BlogPost.init)
}
callback(blogposts)
}
```
缺點(diǎn):
1熬北、代碼重復(fù)
2、這兩個(gè)方法同時(shí)都很難測(cè)試
解決方案:提取共通功能
- 提取共通功能(解決代碼重復(fù))
```
func loadResource<A>(at path: String,parse: (Any) -> A?,callback: (A?) -> ()){
let resourceURL = webserviceURL.appendingPathComponent(path)
let data = try? Data(contentsOf: resourceURL)
let json = data.flatMap {
try? JSONSerialization.jsonObject(with: $0, options: [])
}
callback(json.flatMap(parse))
}
```
上面兩個(gè)函數(shù)基于loadResource重寫:
```
func loadUsers(callback: ([User]?) -> ()) {
loadResource(at: "/users", parse: jsonArray(User.init), callback: callback)
}
func loadBlogPosts(callback: ([BlogPost]?) -> ()) {
loadResource(at: "/blogposts",
parse: jsonArray(BlogPost.init),
callback:callback)
}
func jsonArray<A>(_ transfrom:@escaping (Any) -> A?) -> (Any) -> [A]? {
return { array in
guard let array = array as? [Any] else {
return nil
}
return array.flatMap(transfrom)
}
}
```
缺點(diǎn):loadResource函數(shù)中path和parse耦合非常緊密诚隙,一旦你改變了其中一個(gè)讶隐,你很可能也需要改變另一個(gè)
解決方案:創(chuàng)建泛型數(shù)據(jù)類型
- 創(chuàng)建泛型數(shù)據(jù)類型
```
struct Resource<A> {
let path:String
let parse:(Any) -> A?
}
extension Resource {
func loadSynchronously(callback: (A?) -> ()) {
let resourceURL = webserviceURL.appendingPathComponent(path)
let data = try? Data(contentsOf: resourceURL)
let json = data.flatMap {
try? JSONSerialization.jsonObject(with: $0, options: [])
}
callback(json.flatMap(parse))
}
}
let usersResource: Resource<[User]> =
Resource(path: "/users", parse: jsonArray(User.init))
let postsResource: Resource<[BlogPost]> =
Resource(path: "/posts", parse: jsonArray(BlogPost.init))
```
優(yōu)點(diǎn):很容易添加新的資源而不必創(chuàng)建新的函數(shù)
- 添加一個(gè)異步的處理方法
不需要改變?nèi)魏维F(xiàn)有的描述api接入點(diǎn)的代碼
```
extension Resource {
func loadAsynchronously(callback: @escaping (A?) -> ()) {
let resourceURL = webserviceURL.appendingPathComponent(path)
let session = URLSession.shared
session.dataTask(with: resourceURL) { data, response, error in
let json = data.flatMap {
try? JSONSerialization.jsonObject(with: $0, options: [])
}
callback(json.flatMap(self.parse))
}.resume()
}
}
```
####泛型的工作方式(從編譯器的視角看)
- 標(biāo)準(zhǔn)庫(kù)里面的min函數(shù)
```
func min<T: Comparable>(_ x: T, _ y: T) -> T {
return y < x ? y : x
}
```
對(duì)于這個(gè)函數(shù),編譯器缺乏兩個(gè)關(guān)鍵的信息
1久又、編譯器不知道類型為T的變量的大小
2巫延、編譯器不知道需要調(diào)用的 < 函數(shù)是否有重載,因此也不知道需要調(diào)用的函數(shù)的地址
由于缺乏這兩個(gè)關(guān)鍵的信息地消,導(dǎo)致它不回直接為這個(gè)函數(shù)生成代碼炉峰。
- 怎么解決這些問(wèn)題呢?
當(dāng)編譯器遇到一個(gè)泛型類型的值脉执,會(huì)將它包裝到一個(gè)固定大小的容器區(qū)存儲(chǔ)者疼阔,如果這個(gè)值超過(guò)這個(gè)容器的尺寸,swift將在堆上面申請(qǐng)內(nèi)存半夷,并將指向堆上面該值的引用存儲(chǔ)到容器里婆廊。除此之外,對(duì)于每個(gè)泛型類型的參數(shù)巫橄,編譯器還維護(hù)一個(gè)或多個(gè)目擊表淘邻,其中包含一個(gè)值目擊表,以及類型上每個(gè)協(xié)議約束一個(gè)的協(xié)議目擊表湘换。這些目擊表將被用來(lái)將運(yùn)行時(shí)的函數(shù)調(diào)用動(dòng)態(tài)派發(fā)到正確的實(shí)現(xiàn)去宾舅。
我們剛描述的“編譯一次,動(dòng)態(tài)派發(fā)”的模型是swift泛型系統(tǒng)的重要的設(shè)計(jì)目標(biāo)彩倚,贴浙,但這種做法是有缺點(diǎn)的:運(yùn)行時(shí)性能會(huì)較低,對(duì)于單個(gè)的函數(shù)調(diào)用來(lái)說(shuō)這點(diǎn)開(kāi)銷是可以忽略的署恍,但是因?yàn)榉盒驮趕wift中非常普及,這種開(kāi)銷很容易堆疊起來(lái)造成性能問(wèn)題蜻直,那怎么樣來(lái)避免這個(gè)額外的開(kāi)銷呢盯质?
####泛型特化
- 泛型特化是什么?
泛型特化是指編譯器按照具體的參數(shù)類型概而,比如說(shuō)(Int)呼巷,將min<T>這樣的泛型類型或者函數(shù)進(jìn)行復(fù)制,特化后的函數(shù)可以將針對(duì)Int進(jìn)行優(yōu)化赎瑰,移除所有的額外開(kāi)銷(主要是動(dòng)態(tài)派發(fā)的開(kāi)銷)王悍。泛型特化是在編譯期間由優(yōu)化器完成的。
舉個(gè)??如果你的代碼中經(jīng)常使用Int來(lái)調(diào)用min函數(shù)餐曼,而只調(diào)用一次Float的版本压储,那很有可能只有Int的版本會(huì)被特化處理,min<T>針對(duì)Int的特化版本如下:
```
func min(_ x: Int, _ y: Int) -> Int {
return y < x ? y : x
}
```
泛型雖然可以減小開(kāi)銷但是也有局限性:泛型特化只能在編譯器可以看到泛型類型的全部定義以及想要進(jìn)行特化的類型的時(shí)候才會(huì)生效鲜漩。換言之,只有在使用泛型的代碼和定義泛型的代碼在同一個(gè)文件中時(shí)集惋,泛型特化才能工作孕似。
如果我們想要泛型特化能跨越模塊邊界使用怎么辦呢?swift中有個(gè)半官方的標(biāo)簽@_specialize刮刑,它能讓你將你的泛型代碼進(jìn)行指定版本的特化喉祭,使其在其它模塊中也可以使用。
```
@_specialize(Int)
@_specialize(String)
public func min<T: Comparable>(_ x: T, _ y: T) -> T {
return y < x ? y : x
}
```
需要注意的是:我們添加了public雷绢,因?yàn)閕nternal泛烙、fileprivate、private的api添加@_specialize是沒(méi)有意義的翘紊。因?yàn)樗鼈儗?duì)其它模塊不可見(jiàn)蔽氨。
####參考資料:Swift進(jìn)階