Swift階級(jí)-泛型

泛型編碼的目的

表達(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 ''
2
3
^

泛型.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)階
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市霞溪,隨后出現(xiàn)的幾起案子孵滞,更是在濱河造成了極大的恐慌,老刑警劉巖鸯匹,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坊饶,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡殴蓬,警方通過(guò)查閱死者的電腦和手機(jī)匿级,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)染厅,“玉大人痘绎,你說(shuō)我怎么就攤上這事⌒ち福” “怎么了孤页?”我有些...
    開(kāi)封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)涩馆。 經(jīng)常有香客問(wèn)我行施,道長(zhǎng),這世上最難降的妖魔是什么魂那? 我笑而不...
    開(kāi)封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任蛾号,我火速辦了婚禮,結(jié)果婚禮上涯雅,老公的妹妹穿的比我還像新娘鲜结。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布精刷。 她就那樣靜靜地躺著拗胜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贬养。 梳的紋絲不亂的頭發(fā)上挤土,一...
    開(kāi)封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音误算,去河邊找鬼仰美。 笑死,一個(gè)胖子當(dāng)著我的面吹牛儿礼,可吹牛的內(nèi)容都是我干的咖杂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蚊夫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼诉字!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起知纷,我...
    開(kāi)封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤壤圃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后琅轧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體伍绳,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年乍桂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冲杀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡睹酌,死狀恐怖权谁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情憋沿,我是刑警寧澤旺芽,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站辐啄,受9級(jí)特大地震影響甥绿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜则披,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洗出。 院中可真熱鬧士复,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至冗荸,卻和暖如春承璃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚌本。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工盔粹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人程癌。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓舷嗡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親嵌莉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子进萄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容