泛型编码的目的
表达算法或者数据结构所要求的核心接口。(核心接口是什么呢?也就是找到想要实现的功能的最小需求。)
泛型编码带来的优势:
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 ''
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 {
^
因为使用操作符重载的时候,编译器会使用非泛型的版本,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进阶