原文:What is the “any” keyword in Swift? – Donny Wals
在 Swift 5.6 中,苹果为 Swift 语言添加了一个新的关键字:any。正如你在这篇文章中所看到的,any 关键字的使用与你使用 some 关键字的方式非常相似。它们都被用在协议名称的前面,而且它们都告诉我们一些关于该协议的使用方式。一旦你深入了解 any 的含义,你会发现它与 some 非常不同。事实上,你可能会得出这样的结论:any 与 some 有点相反。在这篇文章中,你将了解到你需要知道的关于 Swift 中的 any 关键字以及存在类型(existential)的一切,以及它们是什么。
让我们在一个非常简单的例子中看看 any 关键字的用途,从而直接进入 any 关键字:
protocol Networking {
func fetchPosts() async throws -> [Post]
// ...
}
struct PostsDataSource {
let networking: any Networking
// ...
}
如果你不熟悉 Swift 的
some关键字或需要复习,请查看这篇关于 Swift 的some关键字的文章。
虽然 any 关键字和 some 关键字看起来很相似,都是用在协议前面,而且听起来它们传达的信息类似于 "只要符合这个协议,我不在乎这个类型用的是什么",但它们其实完全不一样。为了理解它们的区别,我们需要看一下 Swift 中的存在性是什么。
了解什么是 Swift 中的存在类型
some 允许我们写代码,或多或少地忽略或抛弃协议的关联类型和/或 Self 要求,同时期望在返回 some 协议的函数中,每个返回的对象都有相同的具体类型。而 any 关键字只是注释一个给定的类型是一个所谓的存在类型。虽然你可能不知道什么是存在类型,但你可能已经看到它们的使用。例如,如果我们看一下你刚才看到的 PostsDataSource 结构的 "旧" 语法,它看起来如下:
struct PostsDataSource {
let networking: Networking
// ...
}
请注意,我所做的只是删除了 any 关键字。我们使用的 Networking 对象是一个存在类型。这意味着 let networking 是“一个遵守 Networking 协议的对象”。编译器不知道它将是哪个对象,也不知道这个对象的类型是什么。编译器所知道的是,当我们初始化 PostDataSource 时,会有一个对象,任何对象,会被分配给 let networking,而且这个对象遵守 Networking 协议。我们基本上只是确定我们会有一个包含 Networking 对象的盒子。为了确切地知道哪个对象被放在那个盒子里,我们需要在运行时打开那个盒子,偷看里面,并找到那个对象。
重要的是要知道,存在类型的使用成本相对较高,因为编译器和运行时不能预先确定应该为填充存在类型的具体对象分配多少内存。每当你在一个存在类型上调用一个方法,比如你之前看到的代码中的 networking 属性,运行时将不得不动态地把这个调用分配给具体对象,这比直接到具体类型的静态分配要慢。
Swift 团队认为,目前在一个具体的对象上,要实现一个存在类型太容易了。这基本上意味着我们很多人在写代码时都使用了协议(existentials),而我们并没有真正意识到这一点,这损害了我们的性能。例如,你之前看到的老式的 PostDataSource 并没有什么问题,对吗?
struct PostsDataSource {
let networking: Networking
// ...
}
我相信我们都写过这样的代码,事实上,我们甚至认为这是最佳实践,因为我们不依赖于具体类型,这使我们的代码更容易测试和维护。
可悲的是,这段代码通过拥有一个以 Networking 为类型的属性,使用了一个存在类型。这意味着对于运行时来说,并不清楚应该为填补我们的 Networking 属性的对象分配多少内存,而且对 fetchPosts 的任何调用都需要动态调度。
通过引入 any 关键字,语言迫使我们思考这个问题。在 Swift 5.6 中,在我们的 let networking: Networking 前添加 any 修饰符是一个可选项;我们可以按照自己的意愿来做。然而,在 Swift 6 中,我们必须用 any 关键字来修饰存在类型。
动态虽好,但效率不及静态。存在类型带来了性能损耗,但它又是我们目前常用的写法。因此 Swift 5.6 引入了
any关键字,目的就是提醒我们存在类型的负面影响。同时,我们应该尽可能地避免使用any。
从 Swift 6 开始,当我们使用存在类型时,编译器会强制要求使用
any关键字标记,否则会报错。
深入研究 any 关键字
当我阅读 any 的提案时,我意识到 Swift 团队似乎希望我们做的是尽可能地使用泛型和具体类型而不是存在类型。尤其是提案介绍中的这一部分让我明白了这一点。
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. 尽管有这些重要的、通常不受欢迎的影响,但存在类型有一个最小的拼写。在语法上,使用一个类型的代价是隐含的,与泛型约束相似的拼写使许多程序员将存在类型与泛型混淆。实际上,与对泛型的需求相比,对它们所提供的动态性的需求是比较少的,但是语言使得存在类型太容易被触及,尤其是被误触。使用存在类型的代价不应该被隐藏起来,程序员应该明确地选择这些语义。
那么我们应该如何编写我们的 PostsDataSource 而不直接依赖具体的实现呢?我们怎样才能在不使用存在类型的情况下做到这一点,因为存在类型显然是不太理想的?
最简单的方法是给我们的 PostsDataSource 添加一个泛型,并将其约束为遵守 Networking 协议,如下所示:
protocol Networking {
func fetchPosts() async throws -> [Post]
// ...
}
struct PostsDataSource<Network: Networking> {
let networking: Network
// ...
}
通过这样写的代码,编译器将预先知道哪种类型将被用于填充 Network 泛型。这意味着运行时将预先知道需要为这个对象分配多少内存,而且对 fetchPosts 的调用可以静态地而不是动态地进行调度。
提示:如果你对泛型不太熟悉,可以看看这篇文章,了解更多关于 Swift 中的泛型以及它们的使用方法。
如上所示,在编写 PostDataSource 时,你并没有失去任何有价值的东西。你仍然可以为测试注入不同的具体实现,甚至在你的应用程序中也可以有不同网络对象的 PostDataSource 实例。与之前的方法相比,不同的是,当运行时知道你所使用的具体类型(通过泛型实现)时,它可以更有效地执行你的代码。
另外,你也可以重写 let networking 使用 some Networking,而不是使用泛型。要了解更多关于 some 的信息,以及你如何在某些情况下用它来替代泛型,请看这篇文章。
通过不使用 any,我们唯一失去的是在运行时通过给 networking 赋予不同的具体类型的新值来动态更换 networking 的能力(我们无论如何也做不到,因为它被定义为 let)。
有趣的是,由于我们必须在 any 、some 和泛型之间做出选择,当我们定义我们的 let networking 时,选择正确的选项会更容易。我们可以在写 : Networking 的地方写 : any Networking。在 Swift 5.5 和更早的版本中,我们的代码可以正常工作,但我们可能会使用一个次优的存在类型,而不是一个可以从编译时优化和运行时静态调度中获益的具体类型。在某些情况下,这正是你想要的。你可能需要存在类型所提供的灵活性,但通常你会发现你根本就不需要存在类型。
那么 any 关键字到底有多大用处?你是否应该在 Swift 5.6 中就使用它,还是等到编译器在 Swift 6 中开始强制执行 any?
在我看来,any 关键字将为开发者提供一个有趣的工具,迫使他们思考他们如何写代码,更具体地说,我们如何在代码中使用类型。鉴于存在类型对我们代码的性能有不利的影响,我很高兴看到在 Swift 6 以后,我们需要明确地用 any 关键字来注释存在类型。特别是由于经常可以使用泛型来代替存在类型,而不失去使用协议的任何好处。仅仅因为这个原因,就已经值得训练自己在 Swift 5.6 中开始使用 any。
注意:看看我的帖子,比较一下
some和any,了解一下在某些情况下如何用some来代替泛型。
现在在 Swift 5.6 中使用 any 可以让你顺利过渡到 Swift 6。等真正到了 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
// ...
}
如果你真的需要存在类型的 Networking,上述代码至少需要使用 Swift 中的 any Networking 来编写。然而,在大多数情况下,这应该促使你重新考虑使用该协议,而不是使用泛型或编写 some Networking,以提高运行时性能。
使用泛型而非存在类型所带来的性能提升是否足够显著,以至于在普通应用中产生差异,还有待观察。不过,意识到 Swift 中存在类型的代价是好的,这肯定会让我重新考虑我写的一些代码。
Swift 5.7 中的 any 关键字
在 Swift 5.7 中,any 关键字仍然不是所有存在类型的必选项,但某些功能对非 any 协议是不可用的。例如,在 Swift 5.7 中,围绕具有 Self 要求的协议的要求已经放宽了。以前,如果你想使用一个具有 Self 要求的关联类型的协议作为类型,你就必须使用 some。这就是为什么你必须在 SwiftUI 中写 var body: some View。
在 Swift 5.7 中,这一限制被放宽了,但你必须写 any 来使用包含关联类型或 Self 要求的存在类型。下面的例子就是这方面的一个例子:
protocol Content: Identifiable {
var url: URL { get }
}
func useContent(_ content: any Content) {
// ...
}
上面的代码要求我们使用 any Content,因为 Content 扩展了 Identifiable 协议,它有一个关联类型(定义为 associatedtype ID: Hashable)。出于这个原因,如果我们不能使用 some,我们就必须使用 any。
对于使用主要关联类型的协议也是如此。在 Swift 5.7 中,使用带有主要关联类型的存在类型已经需要使用 any 关键字。
请注意,正如我在比较这两个关键词时指出的,any 并不是 some 的替代品。当使用 any 时,你是在选择使用一个存在类型而不是一个具体类型(而这是 some 会提供的)。
尽管 any 在 Swift 6.0 之前不会被完全强制使用,但有趣的是,Swift 5.7 已经要求 any 用于 Swift 5.7 提供的一些新功能。我认为这加强了我在这篇文章中早些时候提出的观点;尽量从今天开始使用 any,这样一旦 Swift 6.0 降临,你就不会被编译器错误吓到。