如何在Swift 5.7中使用范型协议实现动态调度
hudson 译 原文
动态调度是面向对象编程(OOP)中最重要的机制之一。它是使运行时多态性成为可能的核心机制,使开发人员能够编写代码,在运行时而不是编译时决定其执行路径。
尽管在OOP中实现动态调度似乎很容易,但当涉及到面向协议的编程(POP)时,情况并非如此。由于Swift编译器的各种限制,尝试使用协议完成动态调度总是伴随着不可预测的困难。
随着Swift 5.7的发布,所有这些都已成为历史!在POP领域实现动态调度从未如此简单。在本文中,我们将探索从Swift 5.7中获得什么样的改进,以及使用具有关联类型的协议实现动态调度需要什么。
不用多说,直接进入主题!
注意:
如果你不熟悉Swift中的
some
和any
关键字,我强烈建议您首先阅读我的博客文章理解Swift 5.7中的"some"和"any"关键字
准备工作
在我开始向您展示Swift 5.7的改进之前,我们先定义本文中示例代码所需的协议和结构。
swift
struct Gasoline {
let name = "gasoline"
}
struct Diesel {
let name = "diesel"
}
protocol Vehicle {
associatedtype FuelType
var name: String { get }
func startEngin()
func fillGasTank(with fuel: FuelType)
}
struct Car: Vehicle {
let name: String
func startEngin() {
print("\(name) enjin started!")
}
func fillGasTank(with fuel: Gasoline) {
print("Fill \(name) with \(fuel.name)")
}
}
struct Bus: Vehicle {
let name: String
func startEngin() {
print("\(name) enjin started!")
}
func fillGasTank(with fuel: Diesel) {
print("Fill \(name) with \(fuel.name)")
}
}
我们上面的定义与我们在上一篇文章中使用的定义相似,但有一点小小的改变。在Vehicle
车辆协议中,我们有2个功能要求,startEngin()
和 fillGasTank(with:)
。为了演示,我们将尝试在Car
汽车和Bus
(公共汽车)结构中使用这2个功能实现动态调度。
Swift 5.6及以下范型协议的限制
现在,假设我们想创建一个接受异构数组的startAllEngin()
函数,如下所示:
swift
//异构数组:含有"Car"和"Bus"元素的
// 🔴 编译错误: Protocol 'Vehicle' can only be used as a generic constraint because it has Self or associated type requirements
let vehicles: [Vehicle] = [
Car(name: "Car_1"),
Car(name: "Car_2"),
Bus(name: "Bus_1"),
Car(name: "Car_3"),
]
func startAllEngin(for vehicles: [Vehicle]) {
for vehicle in vehicles {
vehicle.startEngin()
}
}
// Execution
startAllEngin(for: vehicles)
您会注意到,这在Swift 5.6中几乎是不可能的,因为系统会提示您一个错误:"Protocol 'Vehicle' can only be used as a generic constraint because it has Self or associated type requirements "。
Swift编译器禁止我们创建一个以Vehicle
为元素类型的异构数组,因为Vehicle
协议有关联类型(FuelType
)。
提示:
如果您想了解有关该错误的更多信息,以及如何在Swift 5.7之前解决它,请查看我在Medium上发表的文章:"Swift:在PATs上完成动态调度(具有关联类型的协议)"
由于苹果对Swift编译器的升级,这一限制在Swift 5.7中不再存在。 我们终于可以使用协议了,就像我们在OOP中使用超类一样。 让我告诉你怎么做。
在简单函数上执行动态调度
在Swift 5.7中,编译器不再禁止创建异构数组。 我们需要做的就是使用any
关键字。
swift
// 使用`any`关键字来指示数组将持有存在类型
let vehicles: [any Vehicle] = [
Car(name: "Car_1"),
Car(name: "Car_2"),
Bus(name: "Bus_1"),
Car(name: "Car_3"),
]
func startAllEngin(for vehicles: [any Vehicle]) {
for vehicle in vehicles {
vehicle.startEngin()
}
}
通过使用any
关键字,我们告诉编译器,数组将包含存在类型,并且其底层具体类型将始终符合车辆协议。
这样,调用startAllEngin(for:)
将给我们带来我们想要的动态调度。
swift
startAllEngin(for: vehicles)
// Output:
// Car_1 enjin started!
// Car_2 enjin started!
// Bus_1 enjin started!
// Car_3 enjin started!
使用范型参数对函数执行动态调度
现在让我们看看另一个更复杂的例子。 假设我们想创建一个名为fillAllGasTank(for:)
的函数。 该函数将根据给定的车辆阵列对车辆的fillGasTank(with:)
函数执行动态调度。
定义一个通用参数类型
起初,我们试图实现的目标可能看起来很简单,但当我们开始编码时,我们会遇到第一个问题:
swift
func fillAllGasTank(for vehicles: [any Vehicle]) {
for vehicle in vehicles {
// 🤔 What to pass in here?
vehicle.fillGasTank(with: ????)
}
}
由于不同类型的车辆需要不同种类的燃料,我们必须创建一个通用协议来代表汽油和柴油。我们继续做吧。
swift
protocol Fuel {
// 限制"FuelType"始终符合"Fuel"协议的类型
associatedtype FuelType where FuelType == Self
static func purchase() -> FuelType
}
Fuel
协议只是一个简单的协议,由一个名为FuelType
的关联类型和一个静态purchase()
函数组成。注意我们如何将FuelType
限制为始终等于符合Fuel
协议的类型。为了使编译器确定静态purchase()
函数返回的具体类型,此约束非常重要。
接下来,让我们使汽油和柴油都符合燃料协议。
swift
struct Gasoline: Fuel {
let name = "gasoline"
static func purchase() -> Gasoline {
print("Purchase gasoline from gas station.")
return Gasoline()
}
}
struct Diesel: Fuel {
let name = "diesel"
static func purchase() -> Diesel {
print("Purchase diesel from gas station.")
return Diesel()
}
}
除此之外,我们还需要确保车辆 Vehicle
协议的FuelType
是符合Fuel
协议的类型。
swift
protocol Vehicle {
// `FuelType` must be type that conform to the `Fuel` protocol
associatedtype FuelType: Fuel
// ...
// ...
}
"any"到"some"的转换
随着燃料协议和所有其他相关更改的到位,我们现在可以重新审视fillAllGasTank(for:)
函数并进行相应进行更新:
swift
func fillAllGasTank(for vehicles: [any Vehicle]) {
for vehicle in vehicles {
// Get the instance of `Fuel` concrete type based on the vehicle's fuel type
let fuel = type(of: vehicle).FuelType.purchase()
// 🔴 Compile error: Member 'fillGasTank' cannot be used on value of type 'any Vehicle'; consider using a generic constraint instead
vehicle.fillGasTank(with: fuel)
}
}
在上述代码中,请注意我们如何利用车辆的燃料类型来获取Fuel
协议的具体类型实例,以便我们可以将其传递到fillGasTank(with:)
函数中。
不幸的是,如果我们尝试编译代码,我们会遇到第二个问题: "Member 'fillGasTank' cannot be used on value of type 'any Vehicle'; consider using a generic constraint instead"。那是什么意思?
为了理解我们遇到的错误,让我们快速回顾一下some
关键字和any
关键字之间的区别。
如上图所示,存在类型的底层具体类型被包裹在一个盒子里。因此,编译器禁止我们访问fillGasTank(with:)
函数。要解决这个问题,在访问fillGasTank(with:)
函数之前,我们必须先将存在类型(开箱)转换为不透明类型。
幸运的是,苹果在Swift 5.7中使转换(开箱)过程变得非常容易。我们需要做的就是将存在类型传递给一个接受不透明类型的函数,转换将自动发生。
swift
func fillAllGasTank(for vehicles: [any Vehicle]) {
for vehicle in vehicles {
// Pass in `any Vehicle` to convert it to `some Vehicle`
fillGasTank(for: vehicle)
}
}
// Create a function that accept `some Vehicle` (opaque type)
func fillGasTank(for vehicle: some Vehicle) {
let fuel = type(of: vehicle).FuelType.purchase()
vehicle.fillGasTank(with: fuel)
}
有了这个函数,我们现在可以正确编译和执行我们的代码,没有任何错误。
swift
fillAllGasTank(for: vehicles)
// Output:
// Purchase gasoline from gas station.
// Fill Car_1 with gasoline.
// Purchase gasoline from gas station.
// Fill Car_2 with gasoline.
// Purchase diesel from gas station.
// Fill Bus_1 with diesel.
// Purchase gasoline from gas station.
// Fill Car_3 with gasoline.
如果您想亲自尝试,请随时在这里获取完整的示例代码。
小结
本文简单介绍了如何实现具有关联类型的协议的动态调度技术。总结一下,要点是基于Swift 5.7的下面几方面改进:
-
消除使用具有关联类型的协议创建异构数组的限制。
-
启用在函数的参数位置使用
any
和some
关键字。 -
从存在类型自动转换为不透明类型,反之亦然。
感谢您的阅读。 👨🏻💻