Swift阶级-泛型

泛型编码的目的

表达算法或者数据结构所要求的核心接口。(核心接口是什么呢?也就是找到想要实现的功能的最小需求。)

泛型编码带来的优势:

1、可以写出可重用的函数和数据结构,比如说Array,Set……

2、可以创建泛型方法,比如说func identity< A > (input: A) -> A

今天的研究内容:

1、如何书写泛型代码

2、谈一谈编译器是如何处理泛型代码

3、如何优化我们的泛型代码

重载

  • 什么是重载?

    重载就是拥有同样的名字,但是参数或者返回类型不同的多个方法互相称为重载方法。

    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)")
}


- 那么问题来了swift是怎么来确定到底使用哪个重载函数呢?

  选择最具体的一个,也就是说非通用的函数会优先于通用函数。    

  ```
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、重载的使用是在编译期间静态决定的,编译器会依据变量的静态类型来决定调用那一个重载,而不是运行时根据值的动态类型来决定的。

    2、但是当使用操作符重载时,编译器会表现出一些奇怪的行为:类型检查器会去使用非泛型的版本,而不考虑泛型版本。

    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)
    }
    

//加一个对整数的重载
func **<I: SignedInteger>(lhs: I, rhs: I) -> I {
// 转换为 IntMax,使用 Double 的重载计算结果,
// 然后用 numericCast 转回原类型
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))
}

如果我们就直接执行2**3,会报错:

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 {
^

因为使用操作符重载的时候,编译器会使用非泛型的版本,2和3自动向上转换为Double 或者Float,由于两者对于整数字面量来说是相同的优先级可选项,所以说编译器无法确定去调用Double的重载还是Float的重载。

解决方法:
1、至少将一个参数显示地声明为整数类型(let intResult = Int(2)**3)

2、明确提供返回值的类型(let intResult:Int = 2**3)

####使用泛型约束进行重载
略写了
####使用泛型进行代码设计
泛型在我们进行程序设计的时会非常有用,它能够帮助我们提取共通的功能,并且减少模版代码。

- 让我们来写一些与网络服务交互的函数:

  获取用户列表的数据,并将它解析为User数据模型(为了简化🌰,使用的是同步方式,在实际开发中,应当使用异步方法加载数据)
  
  ```
  //最原始的方式来实现
  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)
  }

  ```
  如果我们想要写一个相同的函数来加载其它资源,比如说加载博客文章的函数:
  
  ```
  //最原始的方式来实现
  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)
  }

  ```
  
  缺点:
  
  1、代码重复
  
  2、这两个方法同时都很难测试
  
  解决方案:提取共通功能

- 提取共通功能(解决代码重复)

  ```
  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))
  }
  ```
  上面两个函数基于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)
       }
  }

  ```
  
  缺点:loadResource函数中path和parse耦合非常紧密,一旦你改变了其中一个,你很可能也需要改变另一个

  解决方案:创建泛型数据类型
- 创建泛型数据类型

  ```
  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))

  ```
  优点:很容易添加新的资源而不必创建新的函数

- 添加一个异步的处理方法

  不需要改变任何现有的描述api接入点的代码
  
  ```
  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()
      }
  }

  ```
  
####泛型的工作方式(从编译器的视角看)
- 标准库里面的min函数

  ```
  func min<T: Comparable>(_ x: T, _ y: T) -> T {
      return y < x ? y : x
  }
  ```
  对于这个函数,编译器缺乏两个关键的信息
  
  1、编译器不知道类型为T的变量的大小
  
  2、编译器不知道需要调用的 < 函数是否有重载,因此也不知道需要调用的函数的地址
  
  由于缺乏这两个关键的信息,导致它不回直接为这个函数生成代码。
  
- 怎么解决这些问题呢? 

   当编译器遇到一个泛型类型的值,会将它包装到一个固定大小的容器区存储者,如果这个值超过这个容器的尺寸,swift将在堆上面申请内存,并将指向堆上面该值的引用存储到容器里。除此之外,对于每个泛型类型的参数,编译器还维护一个或多个目击表,其中包含一个值目击表,以及类型上每个协议约束一个的协议目击表。这些目击表将被用来将运行时的函数调用动态派发到正确的实现去。
   
   我们刚描述的“编译一次,动态派发”的模型是swift泛型系统的重要的设计目标,,但这种做法是有缺点的:运行时性能会较低,对于单个的函数调用来说这点开销是可以忽略的,但是因为泛型在swift中非常普及,这种开销很容易堆叠起来造成性能问题,那怎么样来避免这个额外的开销呢?
   
####泛型特化
- 泛型特化是什么?

  泛型特化是指编译器按照具体的参数类型,比如说(Int),将min<T>这样的泛型类型或者函数进行复制,特化后的函数可以将针对Int进行优化,移除所有的额外开销(主要是动态派发的开销)。泛型特化是在编译期间由优化器完成的。
  举个🌰如果你的代码中经常使用Int来调用min函数,而只调用一次Float的版本,那很有可能只有Int的版本会被特化处理,min<T>针对Int的特化版本如下:
  
  ```
  func min(_ x: Int, _ y: Int) -> Int {
      return y < x ? y : x
  }

  ```
  泛型虽然可以减小开销但是也有局限性:泛型特化只能在编译器可以看到泛型类型的全部定义以及想要进行特化的类型的时候才会生效。换言之,只有在使用泛型的代码和定义泛型的代码在同一个文件中时,泛型特化才能工作。
  
  如果我们想要泛型特化能跨越模块边界使用怎么办呢?swift中有个半官方的标签@_specialize,它能让你将你的泛型代码进行指定版本的特化,使其在其它模块中也可以使用。
  
  ```
  @_specialize(Int)
  @_specialize(String)
  public func min<T: Comparable>(_ x: T, _ y: T) -> T {
      return y < x ? y : x
  }

  ```
  需要注意的是:我们添加了public,因为internal、fileprivate、private的api添加@_specialize是没有意义的。因为它们对其它模块不可见。

####参考资料:Swift进阶
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容