【译】What is the “any” keyword in Swift?

2,812 阅读10分钟

前言

本文翻译自What is the “any” keyword in Swift?,第一次尝试文章翻译,质量有待提高,欢迎交流

正文

文章已经更新到 Swift 5.7 版本 在 Swift 5.6 中,Apple 为 Swift 引入了一个新的关键字:any。 正如你在本文中所见,关键字 any 的使用是非常类似于 some的。它们都是协议前面的修饰符,并且告诉我们这个协议怎么去使用。一旦你深入去了解 any 是什么的时候,你会发现它和 some 的差异是巨大的。甚至,你会觉得 anysome 完全相反。在本文中,你将会学到关于 any 关键字和 existentials 所有你需要了解的内容以及它们到底是什么。

让我们通过一个简单的示例看下 any 关键字期望地使用方式,并深入研究:

protocol Networking {
    func fetchPosts() async throws -> [Post]
    // ...
}

struct PostsDataSource {
    let networking: any Networking
    // ...
}

💡Tip:如果你不是很熟悉 some 关键字或者需要重新回顾,检索该文章this post on Swift’s some keyword

any 关键字和 some 都是协议前面的修饰符,听起来都像是在表达“我不关心类型如何使用只要它遵循协议约束”这样的意思,从而导致两者看起来比较相像。实际上他们真的完全不一样,为了了解他们之间的差异,我们先来看一看 Swift 中的 existentials 是什么。

了解 Swift 中的 existentials 是什么

some 允许我们去编写一些可以省略的代码,存在关联类型或者 Self 的协议要求并期望函数通过 some Protocol 返回的每一个对象都有相同的具体类型。而 any 关键字只是对给定 existential 类型的标注。 尽管你可能不了解什么是 existential 类型,实际你可能已经见过类似的使用场景了。例如我们用以前的语法方式编写上文的 PostDataSource 结构体,它看起来应该是这样:

struct PostsDataSource {
    let networking: Networking
    // ...
}

请注意,我只是将 any 关键字移除了,这里的 Networking 对象就是我们所说的 existential type。它表示 let networking 是遵循 Networking 的对象。 编译器不知道它具体是那个对象,也不知道该对象的具体类型。仅能确定的是在我们初始化 PostDataSource时将会分配给 let networding 一个对象,分配的对象会遵循 Networking协议。基本可以认为我们能确定会拥有一个盒子,该盒子包含遵守Networking约束的对象,具体到这个被包含的是什么对象,则需要等到运行时拆包才会发现。

非常重要的一点,由于编译器无法在编译期就确认盒子内的对象具体类型以及内存分配方式,导致 existential type 使用起来代价很大。例如上文代码片段中的 networking属性,每当使用 existential type 类型调用方法时, 运行时不得不采用动态派发的方式将消息派发到具体的类型对象上,这比直接静态派发到具体类型要慢的多。

Swift 团队意识到目前实现一个 existential type 对象太过容易。我们很多人在编写 existential type 代码使用过程中并没有意识到这会非常影响我们程序的性能。例如你会觉得先前看到的代码片段不存在任何问题对吧?

struct PostsDataSource {
    let networking: Networking
    // ...
}

我确信所有人都会编写类似的代码,甚至我们可能会认为这是最佳实践方式,因为它没有任何具体类型的依赖,这种代码易于测试以及维护。

很遗憾,这种编写方式因为将 Networking 作为属性类型使用,所以他是 existential type 的。意味着 runtime 运行时不清楚该分配多少内存空间给我们填充的这个对象,对所有 fetchPosts 的调用,都是动态派发的方式。

通过引入关键字 any,Swift 语言强制我们去思考这个问题,在 Swift 5.6 中通过 any 修饰 let networking: 是可选的,我们可以按照我们自己的想法来决定是否使用,然而在 Swift 6 中,使用 any 修饰 existential type 将是被强制使用的。否则会编译报错。

深入了解 any 关键字

当我阅读关于 any 的提案时,我意识到 Swift 团队是想让我们尽可能的使用泛型和具体类型而不是 existential type 来编码。 提案中的这部分内容是我意识到这个事情:

Despite these significant and often undesirable implications, existential types have a minimal spelling. Syntactically, the cost of using one is hidden, and the similar spelling to generic constraints has caused many programmers to confuse existential types with generics. In reality, the need for the dynamism they provided is relatively rare compared to the need for generics, but the language makes existential types too easy to reach for, especially by mistake. The cost of using existential types should not be hidden, and programmers should explicitly opt into these semantics.

虽然 existential types 存在严重问题和不良影响,它扔具有简介的书写方式。 从语法来看,它的使用成本被隐藏了。并且由于和泛型约束类似的格式导致经常和泛型混淆。实际上和泛型使用需求相比,系统对 existential types 提供的动态能力相对较少。但是它实现起来又很容易,尤其是错误的使用方式下。所以不

那么我们应该如何在不直接依赖具体类型实现的情况下编写我们的 PostsDataSource呢?当 existential type 不是一个好选择的时候,我们该怎么做到这一点? 早期的方式是使用一个遵循 Networking 的泛型约束来实现 PostsDataSource,如下:

protocol Networking {
    func fetchPosts() async throws -> [Post]
    // ...
}

struct PostsDataSource<Network: Networking> {
    let networking: Network
    // ...
}

通过这样编码,编译器将会预先确定泛型类型,所以运行时也会提前知道分配多少内存空间给该对象。调用 fetchPosts可以用静态派发替代动态调度。

💡Tip: 如果你不是很熟悉泛型,可以看下这篇文章An introduction to generics in Swift using its built-in types来学习更多关于如何使用泛型的知识。

当用上面的方式实现 PostsDataSource 时,不会损失任何有价值的信息。仍然可以注入不同的具体类型进行测试。即使在 app 内也可以持有存储不同 networking 对象的 PostsDataSource 实例。和前一种通过 existential types 方式相比,运行时通过泛型推到出使用时对象的具体类型,可以更有效率的执行代码。

或者,你也可以使用 some Networking 替代泛型来实现 let networking。关于学习如何使用 some 替代泛型,请参考文章What is the “some” keyword in Swift?

不使用 any 唯一会给我们带来问题是,我们失去了在运行时重新给 'networking' 赋值一个不同具体类型对象的能力(这里因为被定义为 let 所以无论如何也无法赋与新值)。(译者举例):

struct firstNetworking: Networking {
    func fetchPosts() async throws -> [Post]{
        return []
    }
}

struct secondNetworking: Networking {
    func fetchPosts() async throws -> [Post]{
        return []
    }
}

let post = PostsDataSource(networking: firstNetworking())
post.networking = secondNetworking() // 编译报错类型不匹配 

译者补充:因为编译器编译期就通过 PostsDataSource 初始化将 Networking 泛型绑定为 firstNetworking 类型,后续在给成员变量赋值不同具体类型时,会报错 “Cannot assign value of type 'secondNetworking' to type 'firstNetworking‘”, 这就是上面所指的失去了赋值不同具体类型的能力。

有趣的是,当我们定义 let networding 时,我们只须在 anysome 和泛型之间选择。所以很容易做出正确选择。 我们可以在Swift 5.5 及更早的版本中所有使用 : Networking 的地方使用 : any Networking 来代替。我们的代码并不会出错,但是我们可能选择了 existential type 这个次优项,而不是具有编译时优化和运行时静态派发收益的具体的类型。某些场景下,这正是你所需要的,你可能需要 existential type 提供的灵活性,然后你时常并未意识到你根本不需要 existential type。

那么 any 关键字真的有作用吗?应该在 Swift 5.6 之前就使用他还是等到Swift 6时编译器强制要求时更好呢? 在我看来,any 提供一个有趣的工具来强制开发者思考怎么去编写代码,再具体点说,我们应该思考如何在代码中使用类型。鉴于 existential 对我们代码的不利影响,我很高兴的看到我们需要在 Swfit 6 以后使用 any 关键字来显式的修饰该类型。尤其是,当使用泛型替代 existential 类型不会丢失协议的任何信息。仅出于这一个原因,就很值得去学习在 Swfit 5. 6 中使用 any 了。

笔记: 阅读我的文章比较 some 和 any 来更多地了解如何在某些情况下使用 some 来代替泛型

现在在 Swift 5.6 中就使用 any 将很平顺的过渡到 Swift 6, 否则下面的代码示例将会编译报错:

protocol Networking {
    func fetchPosts() async throws -> [Post]
    // ...
}

struct PostsDataSource {
    // This is an error in Swift 6 because Networking is an existential
    let networking: Networking
    // ...
}

如果你真的需要使用 existential type 类型的 Networking, 上面的代码至少要被写成 any Networking 的格式。然后大部分场景下,any 都是在提醒你应该重新考虑使用支持泛型的协议或者使用some Networking来提高运行时的性能效率。

在普通的 App 中使用泛型代替 existentials 带来的性能提升是否有显著效果还有待观察,相比,意识到 existentials 在 Swift 带来的成本更有意义,这无疑会使我重写思考是否需要重构已有的代码。

Swift 5.7 中的 any

Swift 5.7 对于 existentials 中 any 的使用仍然不是强制要求的,但是某些功能不适用于 non-any 的协议。例如,Swift 5.7 先前已经放宽了对含有 Self的协议使用要求。如果你想使用带有关联类型或者 Self 要求的协议作为类型使用,你可以使用 some, 这也是为什么你可以在 SwiftUI 中使用 var body: some View

Swift 5.7中限制已经放宽,但是你仍然需要使用 any 来处理关联类型或者 Self 要求的协议作为 existential 类型使用。例如一下示例:

protocol Content: Identifiable {
    var url: URL { get }
}

func useContent(_ content: any Content) {
    // ...
}

上文的代码要求我们使用 any Content 因为 Content 扩展自 Identifiable 协议,这个协议有关联类型(定义为 associatedtype ID: Hashable), 由此,如果我们不使用 some 的话,则需要使用 any来修饰。

对于使用 primary associated type的协议也是如此, Swift 5.7 中使用 primary asscociated type 的 existential 类型已经要求使用 any关键字。

需要注意的是,就像我的 comparison of these two keywords 文章中指出的那样,any 并不是为了取代 some 关键字。当使用 any 时,你可以实现使用 existential 类型代替具体类型(some提供的能力)。

尽管在 Swift 6.0 之前 any 不会是完全强制性的,但有趣的是 Swift 5.7 对于 Swift 5.7 提供的一些新功能已经需要 any。我认为这强化了我在这篇文章前面提出的观点;尝试从现在开始使用 any,以便当Swift 6.0 发布时,你不会对于出现的编译错误感到奇怪。