如何在Swift 5.7中使用范型协议实现动态调度

如何在Swift 5.7中使用范型协议实现动态调度

hudson 译 原文

动态调度是面向对象编程(OOP)中最重要的机制之一。它是使运行时多态性成为可能的核心机制,使开发人员能够编写代码,在运行时而不是编译时决定其执行路径。

尽管在OOP中实现动态调度似乎很容易,但当涉及到面向协议的编程(POP)时,情况并非如此。由于Swift编译器的各种限制,尝试使用协议完成动态调度总是伴随着不可预测的困难。

随着Swift 5.7的发布,所有这些都已成为历史!在POP领域实现动态调度从未如此简单。在本文中,我们将探索从Swift 5.7中获得什么样的改进,以及使用具有关联类型的协议实现动态调度需要什么。

不用多说,直接进入主题!

注意:

如果你不熟悉Swift中的someany关键字,我强烈建议您首先阅读我的博客文章理解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的下面几方面改进:

  • 消除使用具有关联类型的协议创建异构数组的限制。

  • 启用在函数的参数位置使用anysome关键字。

  • 从存在类型自动转换为不透明类型,反之亦然。

感谢您的阅读。 👨🏻‍💻

相关推荐
大熊猫侯佩16 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩2 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu2 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩3 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩3 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩3 天前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩3 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple