在Swift 5.6的更新list里面看到了existential any的简单介绍,但是由于过于简单导致一头雾水,经过一些查证,这里说一下对这个改动的理解。SE-0335
举例简述
通常我们在使用协议时,是为了保证类型的一致性并遵守一些要求,例如实现某个方法:
protocol ProtocolA {
func test()
}
struct Test: ProtocolA {
func test() {
print("hello")
}
}
let t = Test()
t.test() /// Static Dispatch
复制代码
此时的t
是明确类型的,Swift会对t.test()
的调用使用Static Dispatch,效率更高。
如果我们因为某些情况下无法明确类型(确实会出现这种情况),还想调用test
方法,那么我们会出现这种写法:
let t: ProtocolA = Test()
t.test() /// Dynamic Dispatch
复制代码
此时的t
的类型我们就可以称作existential type,Swift会使用Dynamic Dispatch完成test
调用,当前场景是V-Table调用,这种效率要低于静态派发。
所以在SE-0335中新增了any
关键字来处理existential type的情况,上面的写法在5.6中应该为:
let t: any ProtocolA = Test()
t.test() /// Static Dispatch
复制代码
此时就会走静态派发了。
进一步举例
可能你觉得这种场景很少见,那么我再举两个例子
例子1
struct A {
var obs: [ProtocolA]?
}
let a = A()
a.obs?.map {
/// dynamic dispatch
$0.test()
}
struct B {
/// 引入any关键字
var obs: [any ProtocolA]?
}
let b = B()
b.obs?.map {
/// static dispatch
$0.test()
}
复制代码
可以看到,在日常开发中,上面这个场景经常会遇到,原有的写法会对每个ob
进行动态派发,而引入了any
关键字后,变为了静态派发,提高了效率。
例子2
第二个场景是本人在写基础组件时遇到过的疑惑,可以先看下代码:
func test1(obj: ProtocolA) {
obj.test()
}
func test2<T: ProtocolA>(obj: T) {
obj.test()
}
复制代码
之前很疑惑这两种写法的区别,因为编译层面看没什么差异(当方法使用泛型作为返回值时可以明确类型)。现在,通过上面的分析,我们可以理解了,test1
中方法调用是动态派发,而test2
的方法调用是静态派发,显然后者是更为科学的写法,然后我们在使用的时候前者却更容易理解,所以any
关键字就派上用场了:
func test(obj: any ProtocolA) {
obj.test() /// Static Dispatch
}
复制代码
以上。
后话
目前Xcode 13.3可以使用此特性。对于此特性,按照苹果的计划,在未来的Swift 5.x版本中,将对涉及到的地方进行警告,而在Swift 6开始会直接抛出错误。
这种变化有可能在未来破坏现有代码,所以在过渡期应该尽可能地去使用正确的形式。在个人看来这个改动非常重要,通过理解的过程也可以加深对Protocol类型的理解。