【译】Understanding the “some” and “any” keywords in Swift 5.7

621 阅读6分钟

前言

本文译自Understanding the “some” and “any” keywords in Swift 5.7,可以结合Donny Wals 的几篇文章来一起学习 anysome的异同点。相关的文章翻译也完成,可以作为参考。欢迎指导交流。

正文

someany关键字并不是 Swift 新引入的能力了, some 关键字在 Swift 5.1 版本引入,而 any关键字则是在 Swift 5.6 中引入的。在 Swift 5.7 中,苹果对这两个关键字进行了一项重大改进。我们可以在函数参数位置使用 any 或者 some了。

这项改进不仅使泛型函数看起来更清晰,而且解锁了一些写的方式去编写泛型代码。剧透警告-我们可以和下面的报错信息说拜拜了:

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

想了解更多?继续读下去!

第一件事

在深入细节之前,我们先定义本文中使用到的协议:

 protocol Vehicle {
 ​
     var name: String { get }
 ​
     associatedtype FuelType
     func fillGasTank(with fuel: FuelType)
 }

在这之后,我们定义 CarBus结构体,他们都遵循 Vehicle协议。并且他们需要不同种类的燃料:

 struct Car: Vehicle {
 ​
     let name = "car"
 ​
     func fillGasTank(with fuel: Gasoline) {
         print("Fill (name) with (fuel.name)")
     }
 }
 ​
 struct Bus: Vehicle {
 ​
     let name = "bus"
 ​
     func fillGasTank(with fuel: Diesel) {
         print("Fill (name) with (fuel.name)")
     }
 }
 ​
 struct Gasoline {
     let name = "gasoline"
 }
 ​
 struct Diesel {
     let name = "diesel"
 }

注意CarBus中的 fillGasTank(with:) 函数参数数据类型是不同的。 Car要求GasolineBus要求是Diesel。这也是为什么我们需要在协议 Vehicle 中定义关联类型 FuelType

继续深入细节。

理解 "some" 关键字

some 关键在在 Swift 5.1 中被引入,它和协议一起用来创建 opaque type(不透明类型) 来表示遵循特定协议的内容,当用在函数参数位置时,表示这个函数接收遵循特定协议的某个具体类型的参数。

在这,你可能很迷惑,我们不是已经可以做到这个了吗?

事实上你是对的,在函数参数位置使用some 关键字实际上和使用"<>"或者在句尾使用 where子句是等价的。

 // The following 3 function signatures are identical.
 ​
 func wash<T: Vehicle>(_ vehicle: T) {
     // Wash the given vehicle
 }
 ​
 func wash<T>(_ vehicle: T) where T: Vehicle {
     // Wash the given vehicle
 }
 ​
 func wash(_ vehicle: some Vehicle)  {
     // Wash the given vehicle
 }

当我们在变量定义时使用 some 关键字,我们告诉编译器我们正在使用一个特定的具体类型使其工作。因此,不透明类型表示的隐式类型必须针对变量的生效范围是唯一的。

 var myCar: some Vehicle = Car()
 myCar = Bus() // 🔴 Compile error: Cannot assign value of type 'Bus' to type 'some Vehicle'

有趣的一点是,编译器同样禁止将相同类型的新实例分配给变量。

 var myCar: some Vehicle = Car()
 myCar = Car() // 🔴 Compile error: Cannot assign value of type 'Car' to type 'some Vehicle'
 ​
 ​
 var myCar1: some Vehicle = Car()
 var myCar2: some Vehicle = Car()
 myCar2 = myCar1 // 🔴 Compile error: Cannot assign value of type 'some Vehicle' (type of 'myCar1') to type 'some Vehicle' (type of 'myCar2')

考虑到这点,我们将它与数组一起使用时必须遵守相同的规则:

 // ✅ No compile error
 let vehicles: [some Vehicle] = [
     Car(),
     Car(),
     Car(),
 ]
 ​
 ​
 // 🔴 Compile error: Cannot convert value of type 'Bus' to expected element type 'Car'
 let vehicles: [some Vehicle] = [
     Car(),
     Car(),
     Bus(),
 ]

作为函数的返回类型时也是一样

 // ✅ No compile error
 func createSomeVehicle() -> some Vehicle {
     return Car()
 }
 ​
 ​
 // 🔴 Compile error: Function declares an opaque return type 'some Vehicle', but the return statements in its body do not have matching underlying types
 func createSomeVehicle(isPublicTransport: Bool) -> some Vehicle {
     if isPublicTransport {
         return Bus()
     } else {
         return Car()
     }
 }

这就是 some的全部内容,让我们接下来看下 any关键字,以及二者之间的差异。

理解 "any" 关键字

any 关键字是 Swift 5.6 引入的,引入的目的是为了创建 existential type(存在类型)。 在 Swift 5.6中, any关键字在修饰 existential type 时并非是强制的。但是在 Swift 5.7 中,如果你不这样做,将会造成编译器报错:

 let myCar: Vehicle = Car() // 🔴 Compile error in Swift 5.7: Use of protocol 'Vehicle' as a type must be written 'any Vehicle' 
 let myCar: any Vehicle = Car() // ✅ No compile error in Swift 5.7
 ​
 // 🔴 Compile error in Swift 5.7: Use of protocol 'Vehicle' as a type must be written 'any Vehicle' 
 func wash(_ vehicle: Vehicle)  {
     // Wash the given vehicle
 }
 ​
 // ✅ No compile error in Swift 5.7
 func wash(_ vehicle: any Vehicle)  {
     // Wash the given vehicle
 }

正如苹果工程师解释的那样,existential type 是类似盒子的类型,它包装了遵循特定协议的类型。

Understand-some-any-comparison.png

如上图所示,不透明类型和 existential type(存在类型) 主要的不同点是这个 box 盒子,这个盒子允许我们去存储任意具体类型,只要该隐式的具体类型遵循了特定的协议就可以。因此,允许我们做到不透明类型做不到的事情。

 // ✅ No compile error when changing the underlying data type
 var myCar: any Vehicle = Car()
 myCar = Bus()
 myCar = Car()
 ​
 // ✅ No compile error when returning different kind of concrete type 
 func createAnyVehicle(isPublicTransport: Bool) -> any Vehicle {
     if isPublicTransport {
         return Bus()
     } else {
         return Car()
     }
 }

更好的消息是,在 Swift 5.7 中,我们可以使用any 关键字修饰带关联类型的协议。所以使用带有关联类型的协议创建一个合成元素的异构数组不再是限制。

 // 🔴 Compile error in Swift 5.6: protocol 'Vehicle' can only be used as a generic constraint because it has Self or associated type requirements
 // ✅ No compile error in Swift 5.7
 let vehicles: [any Vehicle] = [
     Car(),
     Car(),
     Bus(),
 ]

这很酷的对吧?

这种改进不仅消除了 "protocol can only be used as a generic constraint because it has Self or associated type requirements" 的错误,而且使带有关联类型的协议完成动态派发更简单。另一篇文章中将会讲解。

额外提示:

在这了解带有关联类型的协议实现动态派发是多容易的事情How to Achieve Dynamic Dispatch Using Generic Protocols in Swift 5.7

"any" 关键字的限制

尽管看起来不错,使用 any 创建 existential type 仍然有它的限制所在。一个重要的限制是,我们不能使用 ==来操作比较两个 existential type 的实例对象。

 // Conform `Vehicle` protocol to `Equatable`
 protocol Vehicle: Equatable {
 ​
     var name: String { get }
 ​
     associatedtype FuelType
     func fillGasTank(with fuel: FuelType)
 }
 ​
 ​
 let myCar1 = createAnyVehicle(isPublicTransport: false)
 let myCar2 = createAnyVehicle(isPublicTransport: false)
 let isSameVehicle = myCar1 == myCar2 // 🔴 Compile error: Binary operator '==' cannot be applied to two 'any Vehicle' operands
 ​
 ​
 let myCar1 = createSomeVehicle()
 let myCar2 = createSomeVehicle()
 let isSameVehicle = myCar1 == myCar2 // ✅ No compile error

仔细想想,这是有道理的。前面也提到过, existential type 可以在盒子内部包装任意具体的类型。对于编译器而言,existential type 就是一个盒子,它不知道盒子内部是什么,因此在无法保证盒子内部是否是相同的类型时,是不可能去比较两个对象的。

你需要了解的另外一个限制是 existential type 的效率低于不透明类型(被 some 修饰的)。Donny Wals已经在它的文章What is the “any” keyword in Swift?中讨论过很多细节,我强烈建议你去看下。

译者补充: What is the “any” keyword in Swift? 这篇文章也已经翻译完成,可以查看掘金文章

尽管苹果给 any 关键字设置了很多改进,但是如果不透明类型可以做到,则仍然建议使用 some 关键字。下图援引自 Source: What’s new in Swift

Screenshot-2022-06-19-at-8.54.04-PM

总结

Swift 5.7 中的改进绝对是受欢迎的,一方面,它极大地改进了我们代码的语法和可读性,另一方面,它以更高效的方式为我们编写泛型代码开辟了新的途径。

我希望这篇文章可以让你很好的理解 anysome,随时在 Twitter 上关注我并订阅我的每月通讯,这样您就不会错过我即将发表的任何文章。

谢谢阅读👨🏻‍💻

引用的 WWDC Sessions: