Swift 5 7 中的 any 和 some 有何区别

241 阅读9分钟

原文:What’s the difference between any and some in Swift 5.7? – Donny Wals

协议(Protocol)是 Swift 语言中极其重要的一部分,在最近的更新中,我们获得了一些有关协议和泛型的新功能,使我们能够更有意识地在代码中使用协议。这是通过 anysome 关键字完成的。

在这篇文章中,你将了解你需要知道的关于这两个关键字差别的一切。我们将从每个关键字的介绍开始,然后你将了解更多关于每个关键字解决的问题,以及你如何决定在你的代码中应该使用 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

由于 anysome 都适用于协议,我想在这篇博文中把它们并列起来,以更好地解释它们所解决的问题,以及你应该如何决定你应该使用 anysome 还是其他东西。

关于 any 关键词的全面概述,请参考这篇文章

了解 anysome 所解决的问题

为了解释 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 Collectionsome Publishersome 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"。区别不大,但写起来容易多了。而且在功能上是等同的。

anysome 之间做出选择

一旦你理解了 anysome 的用例,你就会意识到,这不是一个二选一问题。它们各自解决了非常相似的问题,总有一个更正确的选择。

通常来说,只要有可能,你应该优先使用 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>,有两个原因:

  1. 没有办法确保编译器为 play 方法推导出的具体集合与为 var playlist 推导出的具体集合一致;这意味着它们可能不一样,这将是一个问题。
  2. 编译器无法首先推导出 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 就可以了。

综上所述

虽然 someany 听起来很复杂(说实话也是),但它们也是 Swift 5.7 中非常强大和重要的部分。值得尝试去理解它们,因为你会对 Swift 如何处理泛型和协议有更好的理解。掌握这些主题将真正把你的编码提高到新的水平。

现在,如果有意义的话,应该首选 some 或泛型而不是 anyany 关键字应该只在你真的想使用那个存在型或 box 的时候使用,在那里你需要在运行时偷看盒子里的东西,以便你可以调用方法和访问它的属性。