如何在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关键字。

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

感谢您的阅读。 👨🏻‍💻

相关推荐
Swift社区4 小时前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift
东坡肘子1 天前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
威化饼的一隅2 天前
【多模态】swift-3框架使用
人工智能·深度学习·大模型·swift·多模态
opentogether4 天前
Swift 的动态性
开发语言·ssh·swift
苍墨穹天4 天前
SWIFT基本使用
linux·swift
SchneeDuan5 天前
从源码分析swift GCD_DispatchGroup
ios·swift·源码分析·gcd
请叫我飞哥@6 天前
iOS在项目中设置 Dev、Staging 和 Prod 三个不同的环境
ios·xcode·swift
Cedric_Anik9 天前
iOS渲染概述
ui·ios·swift
hxx2219 天前
iOS swift开发系列--如何给swiftui内容视图添加背景图片显示
ios·swiftui·swift
胖虎19 天前
SwiftUI - (十九)组合视图
ios·swiftui·swift·组合视图