【译】What’s the difference between any and some in Swift 5.7?

383 阅读9分钟

前言

本文翻译自What’s the difference between any and some in Swift 5.7?,主要介绍 some 关键字和 any关键字的区别

正文

协议是 Swift 语言中的重要组成部分,在最近的更新中,围绕协议和泛型增加了一些新的能力,使我们更倾向于在项目代码中使用协议。这就是关于 anysome关键字的内容更新。

在本文中,你将会学习关于这两个关键字的所有你需要知道的内容,我们首先介绍每一个关键字,然后更深入的了解每个关键字解决的问题类型, 最终你会决定你应该在代码中使用 some还是 any

some 关键字

在 Swift 5.1中,苹果引入了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’

或者在更早的 Swift 版本中,是如下报错:

protocol can only be used as a generic constraint because it has Self or associated type requirements

some 关键字修复了这个问题,它向任意将 some Protocol 作为类型的交互对象隐藏了关联的具体类型,这一点我们稍后细谈。

有关 some的完整概述,请参阅这篇

any 关键字

Swift 5.6 中, Swift 语言引入了 any 关键字。

尽管听起来 any 关键字充当了类型擦除的帮手,实际上, 它真正的作用是告知编译器,你正在使用 existential type 存在类型(遵守协议的一个盒类型)作为你的数据类型使用。

你以前编写的代码可能是这样子:

 func getObject() -> SomeProtocol {
   /* ... */
 }

5.6 以及后续版本,需要这样写:

 func getObject() -> any SomeProtocol {
   /* ... */
 }

这显式的指明了你在 getObject中的返回值类型是 existential type (盒类型)而并非是编译期可以推导处的具体的类型。注意,使用 any修饰目前还不是强制要求的,但是你应该从现在就使用它,因为 Swift 6.0 将强制要求使用 any关键字来修饰 existentials,就像你刚刚看到的例子那样。

由于 anysome 两个关键字都可以修饰协议,所以我将在这篇博客文章中,并排对比他们解决的问题。以及你应该如何决定使用 anysome还是其他的方式。

有关 some的完整概述,请参阅这篇

理解 any 和 some 分别解决的问题

为了阐述 any解决的问题,我们将使用一个统一的示例,该示例以一种有意义的方式涵盖了这两个关键字,设想以协议创建一个 Pizza模型:

 protocol Pizza {
     var size: Int { get }
     var name: String { get }
 }

协议很简单,但正是我们需要的。在 Swift 5.6 中,你可能会以下面的方式编写函数以获取一个 pizza

 func receivePizza(_ pizza: Pizza) {
     print("Omnomnom, that's a nice (pizza.name)")
 }

当调用这个函数时, receivePizza 接收一个所谓的披萨盒子类型的参数,为了获取到披萨的名称,Swift 需要解包查看这个披萨盒子,获取实现披萨协议的具体类型,然后才能再获取对应的名字,实际上这样就不会再有针对披萨协议的编译时优化。导致 receivePizza调用比我们想象中在性能上会更昂贵一些。

此外,下面的函数看起来是不是和前面的没啥区别?

 func receivePizza<T: Pizza>(_ pizza: T) {
     print("Omnomnom, that's a nice (pizza.name)")
 }

不过这里还是有一个主要区别的。这里的 Pizza 协议并不是用作类型使用,而是对 T的约束使用的。编译器可以在编译时推导出 T的类型并且 rececivePizza将会接收一个具体类型的实例而不是一个盒类型。

因为这些区别并不是很明确,所以 Swift 团队引入了 any 关键字,关键字并没有新增任何功能,相反,它只是强制要求去明确的表达出这里是一个 existential type 类型:

 func receivePizza(_ pizza: any Pizza) {
     print("Omnomnom, that's a nice (pizza.name)")
 }

使用了泛型 <T: Pizza> 的例子不需要使用 any关键字,因为这里的 Pizza是作为约束条件使用的,并非 exisetential type 类型。

现在我们比较明确的了解了 any,接下来看下 some: 在 Swift 中,部分开发者会编写以下代码:

 let someCollection: Collection

编译器会抛出错误告诉开发者,这个Collection包含 Self Requirement 或者关联类型,在 Swift 5.1 中,我们可以写 some Collection来告诉编译器,任何访问 someCollection的对象都不应该关心绑定的关联类型或者 Self Requirement 这些细节问题。没有关联类型的信息,也没有关于 Self的信息,仅需要知道这些都会遵循 Collection 就足够了。

这个机制是 SwiftUI View 协议工作的必要条件。

缺点是任何访问 some Collectionsome Publisher或者some View的对象都无法使用泛型特性,这个问题已经被 primary associated types解决,可以阅读What are primary associated types in Swift 5.7?了解更多。

然而,并非所有协议都有关联类型要求,例如 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,也不需要 "创建"一个类型来保存我们的 pizza,我们可以说 “这个函数需要 some Pizza” 而不是 “这个函数需要一些被称为 TPizza“。差别不大,但是写法更简洁,而且功能是一样的。

在两者之间做出选择

一旦你明白了 anysome的使用场景,你就会知道这不是二选一的问题,他们分别解决了自身的问题,即使问题比较相似,但是总是有一个更正确的选择。

通常来说,在力所能及的时候你应该优先选择 some 或者泛型而非 any ,通常不要使用一个遵循协议的盒子类型,而是选择遵循协议的具体类型。

还是用 Pizza类比, any将给运行时一个盒子类型表示 Pizza,需要解包这个盒子才知道内部是什么具体类型的披萨,当使用some 或者泛型时,运行时很明确的知道它获取到的是什么类型的披萨,并且立马就知道该如何使用这个类型(如果是夏威夷披萨就扔掉,如果是意大利辣香肠就保留)。

很多场景下,你实际上并不需要 anysome或者泛型就可以正常工作,根据 Swift 团队的说法,如果可以的话,我们应该永远不使用any

实践中做选择

我们再举一个例子来说明这点,这个例子很大程度参考了另一篇文章的内容primary associated types,你需要先读再理解:

 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 playlistany Collection<String> 类型的,没有选择some Collection<String>有两个原因:

  1. 编译器无法保证可以为 play 方法推断出匹配 var platlist 的具体的 collection 类型。这意味着他们可能是不同的类型,这会导致问题。

  2. 编译器无法推断第一个地方的 var playlist: some Collection<String> (你可以试试,这会导致一个编译错误)

    译者补充: 编译报错的原因正如第一条所述,在于并没有限制要求是同一类型,导致 var playlist_ playlist 是允许同时为不同具体类型的变量,所以编译器无法确定是同类型赋值,则会抛出错误。这也是使用 some 替代泛型时需要注意的一点,即最好只有再一处位置使用了泛型时才替代为 some,否则会造成逻辑差别。

    我们可以用下面代码规避 any的使用:

     class MusicPlayer<T: Collection<String>> {
         var playlist: T = []
     ​
         func play(_ playlist: T) {
             self.playlist = playlist
         }
     }
    

    但是这要求我们强制入参和变量使用相同的集合类型 T, 我们可以使用 SetArray 或者其他的 Collection,但是当我们定义 TArray类型是,则无法分配 Set类型给 playlist,但是以前的代码则可以:

     class MusicPlayer {
         var playlist: any Collection<String> = []
     ​
         func play(_ playlist: some Collection<String>) {
             self.playlist = playlist
         }
     }
    

    通过使用 any Collection<String> 我们可以初始化为Array 但是传入参数时使用 Setplay函数。只要传递的是元素类型为 StringCollection集合就可以。

    总结

    虽然 someany 听起来很复杂(尽管确实如此),但是他们扔是 Swift 5.7 版本中重要的一部分。非常有必要去理解他们,因为你可以通过它们更好的理解 Swift 是如何处理泛型和协议的。掌握这些内容可以让你的代码水平更上一层楼。

    现在你知道了, 优先选择 some 或者泛型而不是any更有意义, any关键字只应该在你真的要使用 existential type 或者盒类型时被使用。此时你需要运行时拆包去查看包装的具体类型以便你可以调用它的函数或者访问属性。

    译者补充: 我们通过这篇文章要理解 anysome 的使用场景上的区别,最大的区别在于 any是盒类型,可以包装任意符合要求的具体类型,不仅限于编译时,同样运行时也可以赋值不同的类型。而 some 编译期结束时,就已经推导确认了具体的类型,并且代表的类型是具体且唯一的。所以当另一个 some修饰的类型或者其他具体类型赋值给它时,就会编译错误。

    some和泛型的替换最好是只在一处引用泛型类型时,否则也会有逻辑问题,例如:

     class MusicPlayer<T: Collection<String>> {
         func play(_ playlist: T, _ backup: T) {
         }
     }
    

    如果上文中用 some替换 T

      class MusicPlayer {
         func play(_ playlist: some Collection<String>, _ backup: some Collection<String>) {
         }
     }
    

    则会在一定程度上造成歧义,因为这里的 playlistbackup 可能是不同的类型,而泛型写法中则是相同类型。这一点区别也需要明确注意。