本系列文章将从实践技巧、实现原理以及追踪语言更新等方面对 Swift Protocol 展开深入讨论。主要内容有:
Swift Protocol 背后的故事(实践)
Swift Protocol 背后的故事(理论)
Swift Protocol 背后的故事(Swift 5.6/5.7)
…
本文是系列文章第三篇,简要介绍 Swift 5.6/5.7 在 Protocol 上的相关扩展和优化,主要包括:any
、Opaque Parameter、Unlock existentials for all protocols 以及 Primary Associated Types。
©原创文章,转载请注明出处!
Overview
含有 Self requirements
、Associated Type
的协议不能作为类型。
Protocol ‘Equatable’ can only be used as a generic constraint because it has Self or associated type requirements.
在『 Swift Protocol 背后的故事(实践) 』中介绍了相关的解决方案:
- Type Erasure;
- Opaque Types,并由此引入了
some
关键字。
在『Swift Protocol 背后的故事(理论) 』中详细分析了直接将协议作为类型使用时(Existential Types),编译器是如何处理的,并得出结论:对性能有较大影响。
在 Swift 5.6、5.7 中对上述问题做了不少改进和优化,本文主要对这些改进优化做一个简要介绍:
any
Opaque Parameter
Unlock existentials for all protocols
Primary Associated Types
any
通过前两篇文章的介绍,我们知道对于没有 Self requirements
和关联类型的协议,可以单独作为类型使用,并称之为『 Existential Type 』。
应该说,『 Existential Type 』用起来还是挺香的~
1 | protocol Animal { |
如上,从『 功能使用 』角度看,genericsFeed
、existentialFeed
没有任何区别,都能达到泛型效果。
但从『 代码实现 』角度看,显然 existentialFeed
更简洁明了。
作为开发人员,无意识中更容易写出 existentialFeed
,毕竟简单。
这显然不是好现象,毕竟『 Existential Type 』有性能问题。
Swift 团队也意识到这个问题,故在 Swift 5.6 引入了 any
关键字 (swift-evolution/0335-existential-any):
any
本身没有任何功能,只是一个标记;当把协议作类型用时,在其前添加
any
来显式表明开发人员要使用『 Existential Type 』的意图;目前
any
还不是强制的,但从 Swift 6.0 开始将强制使用any
,否则编译报错。因此,尽早用上any
,以免后期升级成本过高。
1 | struct Farm { |
如上,在协议用作类型的地方加上 any
即可。
相当于显式声明这是个『 Existential Type 』!
这是一个悲壮的故事,意味着性能上损耗!
那么,有没有办法做到:既有泛型的性能又有『 Existential Types 』的简洁?
鱼和熊掌兼得?!
答案是肯定的!请往下看~
Opaque Parameter
还记得在『 Swift Protocol 背后的故事(实践) 』中介绍的 some
吗?
Swift 5.1 引入了 some
关键字,用于 Opaque Result Type。
Swift 5.7 对 some
的功能做了扩展,可用于声明方法参数,即:Opaque Parameter。
1 | struct Farm { |
如上,someFeed
在性能上与 genericsFeed
无任何差别,但更简洁、可读性更好。
Opaque Parameter 可以理解为泛型的简化版本。
关于 Opaque Parameter 有 2 点需要注意:
在代替泛型时,仅适用于泛型类型只使用一次的场景,因为 Opaque Parameter 是匿名的,无法复用:
1
2
3
4
5
6
7
8
9
10
11
12// t1 与 t2 具有相同的类型
//
func isEqualGenerics<T>(t1: T, t2: T) -> Bool where T: Equatable {
t1 == t2
}
// 并不要求 t1、t2 的类型完全相同
// 只要它们都实现了 Equatable 即可
//
func isEqualSome(t1: some Equatable, t2: some Equatable) -> Bool {
t1 == t2
}如上,
isEqualGenerics
中的泛型类型T
使用了2 次,分别用于定义t1
、t2
。
因此,isEqualGenerics
与isEqualSome
并不等价,并且isEqualSome
会报错:
Cannot convert value of type (generic parameter of global function isEqualSome(t1:t2:)) to expected argument type (generic parameter of global function isEqualSome(t1:t2:))与 Opaque Result Type 不同,可以使用不能类型的实例调用具有 Opaque Parameter 的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25struct Cat: Animal {
func eat() {
print("Eat fish!")
}
}
struct Panda: Animal {
func eat() {
print("Eat bamboo!")
}
}
struct Farm {
func feedAnimals() {
let cat = Cat()
let panda = Panda()
someFeed(cat) // ✅
someFeed(panda) // ✅
}
func someFeed(_ animal: some Animal) {
animal.eat()
}
}如上,用
cat
、panda
都可以调用someFeed
方法。
因为,someFeed
本质上是一个泛型方法。
any
VS. some
any
、some
都可以配合协议作类型用。
它们的区别是啥?
我们该如何选择?
其实,通过前两篇文章对『 Opqaue type 』与『 Existential type 』的介绍,它们的区别已经比较清楚了。
下面,我们再来直观感受一下:
如上:
通过
some
定义的变量是『 Opqaue type 』;通过
any
定义的变量是 『 Existential type 』;『 Opqaue type 』是『 匿名 』的、但『真实存在 』的类型,如
someCat
其背后的类型就是Cat
,在方法派发上有较好的性能;正是因为『 Opqaue type 』是『 匿名 』的,像下面这些写法都是不允许的:
1
2
3
4
5var cat: some Animal = Cat() // ✅
cat = Cat() // ❌ Cannot assign value of type 'Cat' to type 'some Animal'
var copyCat: some Animal = cat // ✅
copyCat = cat // ❌ Cannot assign value of type 'some Animal' (type of 'cat') to type 'some Animal' (type of 'copyCat')『 Opqaue type 』类型的变量一旦初始化完成,就不能修改,因此像下面这种写法也是不允许的:
1
2
3
4
5
6var animals: [some Animal] = [
Cat() // ✅
]
var cat1: some Animal = Cat()
animals.append(cat1) // ❌ No exact matches in call to instance method 'append'『 Existential type 』是二次封装后的间接类型,在变量赋值时有一个装箱 (Boxing) 的过程,方法派发一律是动态派发,故性能较差。有失必有得,其灵活性比较好:
1
2
3
4
5var cat: any Animal = Cat()
cat = Cat() // ✅
var copyCat: any Animal = cat // ✅
copyCat = cat // ✅1
2
3
4
5
6var animals: [any Animal] = [
Cat() // ✅
]
var cat1: any Animal = Cat()
animals.append(cat1) // ✅1
2
3
4
5
6
7
8
9
10
11
12
13// ❌ Type of expression is ambiguous without more context
//
let someAnimals: [some Animal] = [
Cat(),
Panda()
]
// ✅
//
let anyAnimals: [any Animal] = [
Cat(),
Panda()
]
Blabla,说这么多,具体开发时该怎么选?
Write some
by default.
只有当 some
不能满足需要时 (也就是当你遇到无法解决的编译错误时),才考虑用 any
。
对了,这是 Apple 的建议。
通过 Opaque Parameter,可以兼得泛型的性能和『 Existential type 』的简洁!
心情是不是美美哒^__^
别急,还有更美的事呢!
Unlock existentials for all protocols
从 Swift 5.7 开始,不再会有类似下面的编译错误:
Protocol ‘Equatable’ can only be used as a generic constraint because it has Self or associated type requirements.
也就是说,所有的协议都可以作为类型用!(Unlock all protocols)
无论是否有 Self requirements
、是否有Associated Type
!
喜!
惊?
在『 Swift Protocol 背后的故事(实践) 』中对上面那个编译错误的解释是:
归根结底是因为 Protocol 是运行时特性,而其附带的 Self requirements
/ Associated Type
却需要在编译时保证。
难道现在这个解释不成立了?
非也!
Apple 解锁所有 Protocols 的出发点是:
协议中除了有跟 Self requirements
、Associated Type
相关的接口,还有不相关的接口。
而那些不相关的接口应该可以被正常使用,如下:
1 | protocol Animal { |
虽然 Animal
含有关联类型 (FeedType
),但其中的 drink
接口与关联类型无关。
因此,drink
理应可以正常调用。
Swift 5.7 就是这么做的,故上面的代码在 Swift 5.7 上可以正常编译 ✅ 。
当然了,与 Self requirements
、 Associated Type
有关的接口还是不能调用,如 func feed(_ feed: FeedType)
:
1 | struct Farm { |
Unlock protocols 虽好,但还不够好。
毕竟,使用上还有限制。
能不能做的更好?!
答案是肯定的!请往下看~
Primary Associated Types
除了上面提到的 Unlock protocols 不能放开了用,还有一类问题也值得我们关注:
1 | struct Farm { |
如上,我们期望 drinkAnimals
能批量的对 Animals 调用 drink
方法。
但事与愿违,因为集合 animals
的元素类型未知,编译器看到的都是 Any
。
传统地,得像下面这样改造:
1 | struct Farm { |
又回到泛型的老路上了!
上述这两个问题,归根结底都是关联类型惹的祸!
此时,就轮到 Primary Associated Types 登场了~
『 Primary Associated Types 』是啥?
不防先来看个例子,直观感受一下:
这是在 Swift <= 5.6 版本上,Sequence
的定义:
1 | public protocol Sequence { |
在 Swift 5.7 上的定义:
1 | public protocol Sequence<Element> { |
因此,可以对 drinkAnimals
作如下改造:
1 | struct Farm { |
简单讲,『 Primary Associated Types 』:
可以通过 『 泛型约束 』的方式将协议中的关联类型曝露出来,如:
1
2
3// Element 是 Sequence 中定义的关联类型
//
public protocol Sequence<Element>在使用时可以指定对关联类型的约束,如:
1
2var animals: any Sequence<Animal>
var animals: some Sequence<Animal>需要注意的是,『 Primary Associated Types 』与『 泛型约束 』不是一回事,在使用时可以不指定,如在 Swift 5.7 上下面的写法也是合法的:
1
var animals: any Sequence // ✅
还可以在 extension 上使用『 Primary Associated Types 』,更简洁了,如:
1
2
3
4
5extension Sequence<String> { }
// Equivalent to:
extension Sequence where Element == String { }
针对上一小节提到的错误:
利用『 Primary Associated Types 』可以做如下改造:
1 | // 将关联类型 FeedType 设定为 Primary Associated Types |
可以看到,指定关联类型约束时,可以是协议,如:
1 | var animals: any Sequence<Animal> // Animal 是协议 |
也可以是具体类型,如:
1 | var animal: any Animal<FeedTypeIMP> // FeedTypeIMP 是 struct |
总之,通过『 Primary Associated Types 』进一步解放了生产力!
Structural opaque result types
在 Swift 5.7 中 Opaque Result Types 样式更加丰富了:
1 | func f0() -> [some P] { /* ... */ } // ✅ on Swift 5.7 |
在 Swift 5.7 之前,上面这些写法都是不允许的。
小结
Swift 5.6 引入
any
用于显式声明『 Existential type 』,避免无心滥用;Swift 5.7 将
some
扩展到方法参数,从而兼得泛型的性能和『 Existential type 』的简洁;Swift 5.7 解锁了所有协议均可作为类型;
关联类型在作用上有不少障碍,Swift 5.7 引入了『 Primary Associated Types 』,使用方可以更方便地设定关联类型的信息;
Swift 5.7 对 Opaque Result Types 支持的样式进行了扩展。
经过 Swift 5.6/5.7 的扩展和优化,Swift 在 Protocol 的使用上效率更高了!
参考资料
swift-evolution/0346-light-weight-same-type-syntax.md at main · apple/swift-evolution · GitHub
swift-evolution/0341-opaque-parameters.md at main · apple/swift-evolution · GitHub
swift-evolution/0328-structural-opaque-result-types.md at main · apple/swift-evolution · GitHub
swift-evolution/0335-existential-any.md at main · apple/swift-evolution · GitHub
swift-evolution/0352-implicit-open-existentials.md at main · apple/swift-evolution · GitHub
Embrace Swift generics - WWDC22 - Videos - Apple Developer
Design protocol interfaces in Swift - WWDC22 - Videos - Apple Developer
Improving the UI of generics - Discussion - Swift Forums
What is the any keyword in Swift?
What is the some keyword in Swift?
What are primary associated types in Swift 5.7?
What’s the difference between any and some in Swift 5.7?
Using the ‘some’ and ‘any’ keywords to reference generic protocols in Swift 5.7 | Swift by Sundell
https://betterprogramming.pub/understanding-the-some-and-any-keywords-in-swift-5-7-19d2cb52eae2