原文:What’s the difference between any and some in Swift 5.7? – Donny Wals
协议(Protocol)是 Swift 语言中极其重要的一部分,在最近的更新中,我们获得了一些有关协议和泛型的新功能,使我们能够更有意识地在代码中使用协议。这是通过 any 和 some 关键字完成的。
在这篇文章中,你将了解你需要知道的关于这两个关键字差别的一切。我们将从每个关键字的介绍开始,然后你将了解更多关于每个关键字解决的问题,以及你如何决定在你的代码中应该使用 some 还是 any。
some 关键字
在 Swift 5.1 中,Apple 引入了 some 关键字。这个关键字是使 SwiftUI 工作的关键,因为 View 协议包含了一个关联类型,这意味着 View 协议不能作为一个类型使用。
下面的代码显示了 View 协议是如何定义的。你会注意到,有一个关联类型 Body:
protocol View {
associatedtype Body: View
@ViewBuilder @MainActor var body: Self.Body { get }
}
如果你尝试写 var body: View 而不是 var body: some View,你会在 Swift 5.7 中看到以下编译器错误:
Use of protocol 'View' as a type must be written 'any View’ 使用协议 'View' 作为一个类型,必须写成 'any View'。
或者在旧版本的 Swift 中,你会看到下面的情况:
protocol can only be used as a generic constraint because it has Self or associated type requirements protocol 只能作为泛型的约束条件使用,因为它有 Self 或关联类型要求。
some 关键字解决了这一问题,它将具体的关联类型隐藏起来,不让任何人与有 some Protocol 作为其类型的对象互动。稍后会有更多关于这个的内容。
关于 some 关键字的完整概述,请参考这篇文章。
any 关键字
在 Swift 5.6 中,any 关键字被添加到 Swift 语言中。
虽然听起来 any 关键字像是一个类型擦除助手,但它真正做的是通知编译器,你正在使用一个存在类型(遵守协议的 box 类型)作为你的类型。
你原本会写成的代码:
func getObject() -> SomeProtocol {
/* ... */
}
在 Swift 5.6 及以上版本中应写成如下:
func getObject() -> any SomeProtocol {
/* ... */
}
这就明确了你从 getObject 返回的类型是一个存在类型(通过协议方式描述的一个 box 类型),而不是一个在编译时就被明确的具体对象。注意,使用 any 目前还不是强制性的,但你应该开始使用它。Swift 6.0 将对类似你刚才看到的那个例子中使用的存在类型强制使用 any。
由于 any 和 some 都适用于协议,我想在这篇博文中把它们并列起来,以更好地解释它们所解决的问题,以及你应该如何决定你应该使用 any、some 还是其他东西。
关于 any 关键词的全面概述,请参考这篇文章。
了解 any 和 some 所解决的问题
为了解释 any 所解决的问题,我们应该看一个有点统一的例子,这将使我们能够以一种有意义的方式涵盖这两个关键词。想象一下下面这个模拟 Pizza 的协议:
protocol Pizza {
var size: Int { get }
var name: String { get }
}
这是一个简单的协议,但它是我们所需要的全部。在 Swift 5.6 中,你可能会写下面的函数来接收一个 Pizza :
func receivePizza(_ pizza: Pizza) { // Pizza 协议作为 box 类型使用
print("Omnomnom, that's a nice \(pizza.name)")
}
当这个函数被调用时,receivePizza 函数会收到一个所谓的 Pizza 的 box 类型。为了访问披萨的名字,Swift 必须打开这个盒子,抓住实现 Pizza 协议的具体对象,然后访问 name 属性 。这意味着在 Pizza 上几乎没有任何编译时优化,这使得 receivePizza 方法比我们希望的更昂贵。
此外,下面这个函数看起来也差不多,对吗?
func receivePizza<T: Pizza>(_ pizza: T) { // Pizza 协议作为泛型约束条件使用
print("Omnomnom, that's a nice \(pizza.name)")
}
但这里有一个主要的区别:这里的 Pizza 协议并不是作为一种类型使用的。编译器将能够在编译时解决 T 的类型,而 receivePizza 将收到一个类型的具体实例,而不是一个 box 类型。
因为这种区别并不总是很清楚,所以 Swift 团队引入了 any 关键字。这个关键字并没有增加任何新的功能。相反,它迫使我们清楚地传达这是一个存在类型:
func receivePizza(_ pizza: any Pizza) { // any 强调了 Pizza 协议是一个“存在体”
print("Omnomnom, that's a nice \(pizza.name)")
}
使用泛型 <T: Pizza> 的例子不需要 any 关键字,因为 Pizza 是作为一个约束条件而不是作为一个存在类型。
现在我们对 any 有了更清晰的认识,让我们仔细看看 some。
在 Swift 中,很多开发者都试图写这样的代码:
let someCollection: Collection
只是面对编译器的错误,告诉他们 Collection 有一个 Self 或关联类型的要求。在 Swift 5.1 中,我们可以写 some Collection 来告诉编译器,任何访问 someCollection 的人都不应该关心关联类型和 / 或 Self 要求的具体细节。他们应该只知道这个东西符合 Collection 的要求,仅此而已。没有关于关联类型的信息,也没有关于 Self 的信息可用。
这一机制对于使 SwiftUI 的 View 协议发挥作用至关重要。
当然,缺点是任何使用 some Collection、some Publisher 或 some View 的人都不能访问任何泛型的具体化实现。这个问题通过主要关联类型来解决,你可以在这里读到更多的信息。
然而,并不是所有的协议都有关联类型要求。例如,我们的 Pizza 协议没有关联类型要求,但在某些情况下它可以从 some 中受益。
再考虑一下这个 receivePizza 版本:
func receivePizza<T: Pizza>(_ pizza: T) {
print("Omnomnom, that's a nice \(pizza.name)")
}
我们定义了一个泛型 T,以允许编译器为给定的 Pizza 的具体类型进行优化。some 关键字也允许编译器在编译时知道 some 对象的底层类型是什么;它只是对对象的使用者隐藏了这一点。这也正是 <T: Pizza> 所做的。我们只能在 T 上访问 Pizza 所暴露的东西。这意味着我们可以把 receivePizza<T: Pizza>(_:) 改写成如下:
func receivePizza(_ pizza: some Pizza) {
print("Omnomnom, that's a nice \(pizza.name)")
}
我们在其他地方不需要 T,所以我们不需要特地“创建”一个类型来存放我们的比萨饼。我们可以直接说 "这个函数需要 some Pizza",而不是 "这个函数需要一些 Pizza,我们称之为 T"。区别不大,但写起来容易多了。而且在功能上是等同的。
在 any 和 some 之间做出选择
一旦你理解了 any 和 some 的用例,你就会意识到,这不是一个二选一问题。它们各自解决了非常相似的问题,总有一个更正确的选择。
通常来说,只要有可能,你应该优先使用 some 或泛型而不是 any。你往往不想使用一个符合协议的 box;你想使用符合协议的对象。
或者坚持我们的比萨饼的比喻,any 会递给运行时一个写着 Pizza 的盒子,它需要打开盒子看看里面是哪个比萨饼。有了 some 或泛型,运行时将确切地知道它刚刚得到的是哪一个比萨饼,并且它将立即知道如何处理它(如果是夏威夷风味的就扔掉,如果是意大利辣香肠味的就保留)。
在很多情况下,你会发现你实际上并不打算使用 any,但可以让 some 或泛型发挥作用,而根据 Swift 团队的说法,如果可以的话,我们应该总是选择不使用 any。
在实践中做出决定
让我们再用一个例子来说明这个问题,这个例子在很大程度上借鉴了我对主要关联类型的解释。你应该先读一读,以充分理解这个例子:
class MusicPlayer {
var playlist: any Collection<String> = []
func play(_ playlist: some Collection<String>) {
self.playlist = playlist
}
}
在这段代码中,我使用了 some Collection<String>,而不是 func play<T: Collection<String>>(_ playlist: T),因为这个泛型只用在一个地方。
我的 var playlist 是一个 any Collection<String> 而不是 some Collection<String>,有两个原因:
- 没有办法确保编译器为
play方法推导出的具体集合与为var playlist推导出的具体集合一致;这意味着它们可能不一样,这将是一个问题。 - 编译器无法首先推导出
var playlist: some Collection<String>的内容(试试吧,你会得到一个编译器错误)
我们可以避免 any,写成下面的 MusicPlayer:
class MusicPlayer<T: Collection<String>> {
var playlist: T = []
func play(_ playlist: T) {
self.playlist = playlist
}
}
我们可以使用一个集合,一个数组,或者另一个集合,但是如果 T 被推断为数组,我们就不能把一个集合分配给播放列表。如果按照以前的实现方式,我们可以:
class MusicPlayer {
var playlist: any Collection<String> = []
func play(_ playlist: some Collection<String>) {
self.playlist = playlist
}
}
通过在这里使用 any Collection<String>,我们可以在初始化时将它设置为数组,但未来使用时传递一个 Set 来调用 play 方法,只要传递的对象是一个带有 String 元素的 Collection 就可以了。
综上所述
虽然 some 和 any 听起来很复杂(说实话也是),但它们也是 Swift 5.7 中非常强大和重要的部分。值得尝试去理解它们,因为你会对 Swift 如何处理泛型和协议有更好的理解。掌握这些主题将真正把你的编码提高到新的水平。
现在,如果有意义的话,应该首选 some 或泛型而不是 any。any 关键字应该只在你真的想使用那个存在型或 box 的时候使用,在那里你需要在运行时偷看盒子里的东西,以便你可以调用方法和访问它的属性。