swift

Swift进阶十一:泛型

2022-06-10  本文已影响0人  Trigger_o

一:重载

重载和泛型紧密相关.
拥有同样名字,但是参数或返回类型不同的多个方法互相称为重载方法,方法的重载并不意味着泛型。不过和泛型类似,我们可以将多种类型使用在同一个接口上.

比如定义下面这两个方法

func raise(_ base: Double, to exponent: Double) -> Double {  
      return pow(base, exponent) 
}
func raise(_ base: Float, to exponent: Float) -> Float {
      return powf(base, exponent) 
}

当 raise 函数被调用时,编译器会根据参数和/或返回值的类型为我们选择合适的重载.

let double = raise(2.0, to: 3.0) // 8.0 
type(of: double) // Double 
let float: Float = raise(2.0, to: 3.0) // 8.0 
type(of: float) // Float

Swift 有一系列的复杂规则来确定到底使用哪个重载函数,这套规则基于函数是否是泛型,以及传入的参数是怎样的类型来确定使用优先级。整套规则十分复杂,不过它们可以被总结为一句话,那就是 “选择最具体的一个”。也就是说,非通用的函数会优先于通用函数被使用。

重载是编译时静态决定的

下面两个自由函数的重载方法,也就是定义在类和协议之外的方法

func log<View: UIView>(_ view: View) { 
      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)") 
}

传入label会调用第二个,传入别的就会调用第一个

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)

如果放在UIView类型的数组中就不一样了

let views = [label, button] // Type of views is [UIView] 
for view in views { 
      log(view) 
}
/* It's a UILabel, frame: (20.0, 20.0, 200.0, 32.0) 
    It's a UIButton, frame: (0.0, 0.0, 100.0, 50.0) */

view 的静态类型是 UIView,UILabel 本来应该使用更专⻔的另一个重载,但是因为重载并不会考虑运行时的动态类型,所以两者都使用了 UIView 的泛型重载。
如果你需要的是运行时的多态,也就是说,你想让函数基于变量实际指向的内容决定使用哪个函数,而不考虑变量本身的类型是什么的话,你应该使用定义在类型上的方法,而不是自由函数。比如你可以将 log 定义到 UIView 和 UILabel 上去.

然后看第二个例子,确定一个数组中的所有元素是不是都被包含在另一个数组中.
SetAlgebra协议有一个isSubset(of:)方法,但是使用范围很窄,数组没有实现这个协议

给Sequence添加一个方法,判断元素满足Equatable的序列是否是other的子集

extension Sequence where Element : Equatable{
    func isSubset(of other: [Element]) -> Bool {
        for element in self {
            guard other.contains(element) else { return false }
        }
        return true
    }
}

let oneToThree = [1,2,3]
let fiveToOne = [5,4,3,2,1]
oneToThree.isSubset(of: fveToOne) // true

这个方法复杂度是O(nm),如果缩小范围,可以写成性能更高的方案,

extension Sequence where Element: Hashable {
    func isSubset(of other: [Element]) -> Bool {
        let otherSet = Set(other)
        for element in self {
            guard otherSet.contains(element) else { return false }
        }
        return true
    }
}

现在把Equatable换成Hashable,可以使用Set的特性去匹配,现在otherSet.contains是O(1)的,因此整体是O(n)的.

前面的isSubset都需要一个数组,但还可以再放宽,另外self和other也不需要是同样的类型,只要是Sequence并且元素类型相同都可以.

extension Sequence where Element: Hashable {
    func isSubset<S: Sequence>(of other: S) -> Bool where S.Element == Element {
        let otherSet = Set(other)
        for element in self {
            guard otherSet.contains(element) else { return false }
        }
        return true
    }
}

这样就完成了用泛型重载实现对方法的通用化扩展.
泛型重载的优点是使用时不需要考虑更多细节,只要类型对应上,编译不出错基本就没问题了.
而用闭包实现函数行为参数化,这种方式定制性更强,更灵活但是使用起来更复杂一些.

二:使用泛型进行代码设计

获取用户列表的数据,并将它解析为 User 数据类型。我们创建一个 loadUsers 函数,它可以从网上异步加载用户,并且在完成后通过一个回调来传递获取到的用户列表。
这里简化了获取数据的方式,写成了同步的.

func loadUsers(callback: ([User]?) -> ()) {
        let data = try? Data(contentsOf: .init(fileURLWithPath: ""))
        let json = data.compactMap {
            try? JSONSerialization.jsonObject(with: $0, options: [])
        }
        let users = (json as? [Any]).compactMap { jsonObject in
             jsonObject.compactMap(User.init)
        }
        callback(users)
}

同样再写一个获取文章列表的例子,代码几乎一样,只有User换成BlogPost,以及URL不相同

func loadBlogPosts(callback: ([BlogPost])? -> ())

现在开始提取共通的功能,

func loadResource<A>(at path: String, parse: (Any) -> A?, callback: ([A]?) -> ()) {
        let data = try? Data(contentsOf: .init(fileURLWithPath: path))
        let json = data?.compactMap {_ in
            try? JSONSerialization.jsonObject(with: $0, options: [])
        }
        callback(json.compactMap(parse))
}

使用

func loadUsers(callback: ([User]?) -> ()) {
        loadResource(at: "/users", parse: {User.init(data:$0)}, callback: callback)
}

func loadBlogPosts(callback: ([BlogPost]?) -> ()) {
     loadResource(at: "/posts", parse: jsonArray(BlogPost.init(data:$0)), callback: callback)
 }

最后一个问题,现在这个方法还有两个耦合的部分,parse和path,这俩需要同时修改,我们可以通过构建泛型数据结构来优化

struct Resource<A> { 
      let path: String 
      let parse: (Any) -> A? 

    func loadResource<A>(callback: ([A]?) -> ()) {
            let data = try? Data(contentsOf: .init(fileURLWithPath: path))
            let json = data?.compactMap {_ in
                  try? JSONSerialization.jsonObject(with: $0, options: [])
            }
            callback(json.compactMap(parse))
      }
}

使用

let usersResource: Resource<[User]> = Resource(path: "/users", parse: {User.init(data:$0)})
usersResource.loadResource{

}

三:泛型的工作方式

这是min函数

func min<T: Comparable>(_ x: T, _ y: T) -> T { 
      return y < x ? y : x 
}

min 的两个参数和返回值泛型的唯一约束是它们三者都必须是同样的类型 T,而这个 T 需要满足 Comparable。只要满足这个要求,T 可以是任意类型,它可以是 Int,Float,String 或者甚至是在编译时未知的定义在其他模块的某个类型。也就是说,编译器缺乏两个关键的信息,这导致它不能直接为这个函数生成代码:
1.编译器不知道 (包括参数和返回值在内的) 类型为 T 的变量的大小
2.编译器不知道需要调用的 < 函数是否有重载,因此也不知道需要调用的函数的地址

Swift 通过为泛型代码引入一层间接的中间层来解决这些问题。当编译器遇到一个泛型类型的值时,它会将其包装到一个容器中.
对于每个泛型类型的参数,编译器还维护了一系列一个或者多个所谓的目击表 (witness table):其中包含一个值目击表,以及类型上每个协议约束一个的协议目击表。这些目击表 (也被叫做vtable) 将被用来将运行时的函数调用动态派发到正确的实现去.

对于任意的泛型类型,总会存在值目击表,它包含了指向内存申请,复制和释放这些类型的基本操作的指针。这些操作对于像是 Int 这样的原始值类型来说,只是简单的内存复制,对于引用类型来说,这里也会包含引用计数的逻辑。
我们这个例子中的泛型类型 T 将会包含一个协议目击表,因为 T 有 Comparable 这一个约束。对于这个协议声明的每个方法或者属性,协议目击表中都会含有一个指针,指向该满足协议的类型中的对应实现。在泛型函数中对这些方法的每次调用,都会在运行时通过目击表准换为方法派发。在我们的例子中,y < x 这个表达式就是以这种方式进行派发的.
协议目击表提供了一组映射关系来表达泛型类型满足的协议。这也是说泛型和协议是紧密着联系的原因.

上一篇 下一篇

猜你喜欢

热点阅读