【问题标题】:Swift: Nested type erasureSwift:嵌套类型擦除
【发布时间】:2017-07-08 15:20:10
【问题描述】:

使用 Swift 3.0(如果对我有帮助的话,我可以使用 Swift 4.0……但我认为不会)我想键入 Erase 两个级别。我要键入什么擦除具有关联类型的协议,该协议符合本身具有关联类型的协议。所以有人可以说我想键入擦除 nested 关联类型。

下面的代码是我的代码的一个极其简化的版本,但这样更清楚。所以我真正想要的是这样的:

原始场景 - 未解决

protocol Motor {
    var power: Int { get } 
}

protocol Vehicle {
    associatedType Engine: Motor
    var engine: Engine { get }
}

protocol Transportation {
    associatedType Transport: Vehicle
    var transport: Transport { get }
}

然后我想输入erase Transportation 并能够存储AnyTransportation 的数组,该数组可以有任何Vehicle,而Motor 又可以有任何Motor

所以这是一个包含 3 个协议的场景,其中 2 个具有 (nested) 关联类型。

我不知道该怎么做。实际上,我什至不知道如何解决更简单的情况:

简化方案 - 未解决

我们可以将上面的原始场景简化为我们有 2 个协议的版本,其中只有 1 个有关联类型:

protocol Vehicle {
    var speed: Int { get }
}

protocol Transportation {
    associatedtype Transport: Vehicle
    var transport: Transport { get }
    var name: String { get }
}

然后假设我们有一个符合VehicleBus

struct Bus: Vehicle {
    var speed: Int { return 60 }
}

然后我们有两个不同的BusLinesRedBusLineBlueBusLine 都符合 Transportation

struct RedBusLine: Transportation {
    let transport: Bus
    var name = "Red line"
    init(transport: Bus = Bus()) {
        self.transport = transport
    }
}

struct BlueBusLine: Transportation {
    let transport: Bus
    var name = "Blue line"
    init(transport: Bus = Bus()) {
        self.transport = transport
    }
}

然后我们可以使用bignerdranch here 所描述的基本和盒子模式和类来输入erase Transportation

final class AnyTransportation<_Transport: Vehicle>: Transportation {
    typealias Transport = _Transport
    private let box: _AnyTransportationBase<Transport>
    init<Concrete: Transportation>(_ concrete: Concrete) where Concrete.Transport == Transport {
        box = _AnyTransportationBox(concrete)
    }
    init(transport: Transport) { fatalError("Use type erasing init instead") }
    var transport: Transport { return box.transport }
    var name: String { return box.name }
}

final class _AnyTransportationBox<Concrete: Transportation>: _AnyTransportationBase<Concrete.Transport> {
    private let concrete: Concrete
    init(_ concrete: Concrete) { self.concrete = concrete; super.init() }
    required init(transport: Transport) { fatalError("Use type erasing init instead") }
    override var transport: Transport { return concrete.transport }
    override var name: String {return concrete.name }
}

class _AnyTransportationBase<_Transport: Vehicle> : Transportation {
    typealias Transport = _Transport
    init() { if type(of: self) == _AnyTransportationBase.self { fatalError("Use Box class") } }
    required init(transport: Transport) { fatalError("Use type erasing init instead") }
    var transport: Transport { fatalError("abstract") }
    var name: String { fatalError("abstract") }
}

然后我们可以将RedBusLineBlueBusLine 放入

let busRides: [AnyTransportation<Bus>] = [AnyTransportation(RedBusLine()), AnyTransportation(BlueBusLine())]
busRides.forEach { print($0.name) } // prints "Red line\nBlue line"

在上面链接的关于类型擦除的博客文章中,我想要的实际上是Homogeneous Requirement 的解决方法。

假设我们有另一个Vehicle,例如FerryFerryLine

struct Ferry: Vehicle {
    var speed: Int { return 40 }
}

struct FerryLine: Transportation {
    let transport: Ferry = Ferry()
    var name = "Ferry line"
}

我想我们现在想输入erase Vehicle?因为我们想要一个AnyTransportation&lt;AnyVehicle&gt; 的数组,对吧?

final class AnyVehicle: Vehicle {
    private let box: _AnyVehicleBase
    init<Concrete: Vehicle>(_ concrete: Concrete) {
        box = _AnyVehicleBox(concrete)
    }
    var speed: Int { return box.speed }
}

final class _AnyVehicleBox<Concrete: Vehicle>: _AnyVehicleBase {
    private let concrete: Concrete
    init(_ concrete: Concrete) { self.concrete = concrete; super.init() }
    override var speed: Int { return concrete.speed }
}

class _AnyVehicleBase: Vehicle {
    init() { if type(of: self) == _AnyVehicleBase.self { fatalError("Use Box class") } }
    var speed: Int { fatalError("abstract") }
}

// THIS DOES NOT WORK
let rides: [AnyTransportation<AnyVehicle>] = [AnyTransportation(AnyVehicle(RedBusLine())), AnyTransportation(AnyVehicle(FerryLine()))] // COMPILE ERROR: error: argument type 'RedBusLine' does not conform to expected type 'Vehicle'

当然这不起作用...因为AnyTransportation 期望传入符合Transportation 的类型,但AnyVehicle 当然不符合它。

但我还没有找到解决方案。有吗?

问题 1:是否可以在 Simple Scenario 中键入擦除:[AnyTransportation&lt;AnyVehicle&gt;]

问题2:如果简单场景是可解的,那么原来的场景也可以解吗?

下面仅对我想通过原始方案实现的目标进行更详细的说明

原始场景 - 扩展

我最初的需要是把任何Transportation,有任何Vehicle,本身有任何Motor放在同一个数组中:

let transportations: [AnyTransportation<AnyVehicle<AnyMotor>>] = [BusLine(), FerryLine()] // want to put `BusLine` and `FerryLine` in same array

【问题讨论】:

标签: ios swift swift3 type-erasure


【解决方案1】:

如果您想用任何车辆和任何引擎表达任何运输,那么您需要 3 个盒子,每个盒子都使用“以前的”类型擦除包装。您不希望在任何这些框中使用通用占位符,因为您想谈论完全异构的实例(例如,不是任何具有 特定 Vehicle 类型的交通工具,或任何具有 特定 Motor 类型)。

此外,您可以使用闭包代替使用类层次结构来执行类型擦除,这允许您捕获基本实例而不是直接存储它。这允许您从原始代码中删除大量样板。

例如:

protocol Motor {
    var power: Int { get }
}

protocol Vehicle {
    associatedtype Engine : Motor
    var engine: Engine { get }
}

protocol Transportation {
    associatedtype Transport : Vehicle
    var transport: Transport { get }
    var name: String { get set }
}

// we need the concrete AnyMotor wrapper, as Motor is not a type that conforms to Motor
// (as protocols don't conform to themselves).
struct AnyMotor : Motor {

    // we can store base directly, as Motor has no associated types.
    private let base: Motor

    // protocol requirement just forwards onto the base.
    var power: Int { return base.power }

    init(_ base: Motor) {
        self.base = base
    }
}

struct AnyVehicle : Vehicle {

    // we cannot directly store base (as Vehicle has an associated type). 
    // however we can *capture* base in a closure that returns the value of the property,
    // wrapped in its type eraser.
    private let _getEngine: () -> AnyMotor

    var engine: AnyMotor { return _getEngine() }

    init<Base : Vehicle>(_ base: Base) {
        self._getEngine = { AnyMotor(base.engine) }
    }
}

struct AnyTransportation : Transportation {

    private let _getTransport: () -> AnyVehicle
    private let _getName: () -> String
    private let _setName: (String) -> Void

    var transport: AnyVehicle { return _getTransport() }
    var name: String {
        get { return _getName() }
        set { _setName(newValue) }
    }

    init<Base : Transportation>(_ base: Base) {
        // similar pattern as above, just multiple stored closures.
        // however in this case, as we have a mutable protocol requirement,
        // we first create a mutable copy of base, then have all closures capture
        // this mutable variable.
        var base = base
        self._getTransport = { AnyVehicle(base.transport) }
        self._getName = { base.name }
        self._setName = { base.name = $0 }
    }
}

struct PetrolEngine : Motor {
    var power: Int
}

struct Ferry: Vehicle {
    var engine = PetrolEngine(power: 100)
}

struct FerryLine: Transportation {
    let transport = Ferry()
    var name = "Ferry line"
}

var anyTransportation = AnyTransportation(FerryLine())

print(anyTransportation.name) // Ferry line
print(anyTransportation.transport.engine.power) // 100

anyTransportation.name = "Foo bar ferries"
print(anyTransportation.name) // Foo bar ferries

请注意,尽管Motor 没有任何关联类型,但我们仍然构建了AnyMotor。这是因为protocols don't conform to themselves,所以我们不能使用Motor 本身来满足Engine 关联类型(需要: Motor)——我们目前必须为它构建一个具体的包装器类型。

【讨论】:

  • 非常感谢您的精彩回答,这对我来说意义重大!真的感谢!然而,我的版本比我原来的问题简化了很多,简短的版本:我们可以使用带有结构的类型擦除模式来键入 setters 吗?这就是我使用 class/Base/Box 模式的原因。
  • @Sajjon 当然!在初始化器中,只需说出var base = base,然后存储,例如,执行{ base.name = $0 }_setName 闭包(然后在name 的设置器中转发到此闭包)。我将更新我的答案以显示一个示例。
  • 对!我会尝试翻译成我自己的问题域,如果它不起作用,我会回复你。我赞成并接受您的回答,再次感谢!
  • 是否可以键入 Erase 初始化器,例如init(foo: Int)? :O
  • @Sajjon 尝试将初始化程序(或静态方法)添加到上面给出的类型擦除器之一的问题是没有底层的具体类型可以将调用转发到,所以通常没有合理的方法实施它。您需要一个完全独立的类型擦除器来表示类型擦除的P.Type 元类型,并将协议的静态要求作为实例成员。尽管话虽如此,但对于您的潜在问题可能有更好的解决方案,但是如果没有看到有关您要解决的具体问题的更多背景信息,就很难确定。
【解决方案2】:

Hamish 的解决方案绝对是按照您的要求做的正确方法,但是当您遇到这么多类型擦除时,您需要问自己一些问题。

让我们从最后开始:

let transportations: [AnyTransportation<AnyVehicle<AnyMotor>>] = [BusLine(), FerryLine()] // want to put `BusLine` and `FerryLine` in same array

transportations 可以做什么?说真的,你会写什么代码在不做as?检查的情况下迭代它?唯一可用的通用方法是name。你不能真正调用其他任何东西,因为类型在编译时会不匹配。

这与我的Beyond Crusty 谈话中的示例非常接近,我认为您应该在同一个地方寻找解决方案。例如,而不是这样:

struct RedBusLine: Transportation {
    let transport: Bus
    var name = "Red line"
    init(transport: Bus = Bus()) {
        self.transport = transport
    }
}

考虑如下所示的解决方案(即没有协议,所有 PAT 问题都消失了):

let redBusLine = Transportation(name: "Red line",
                                transport: Vehicle(name: "Bus", 
                                                   motor: Motor(power: 100))

接下来,认真思考您是否将Bus 视为结构。具有相同属性的两条总线是同一条总线吗?

let red = Bus()
let blue = Bus()

红色和蓝色是同一辆公共汽车吗?如果不是,那么这不是值类型。这是一个引用类型,应该是一个类。很多关于 Swift 的讨论都将我们推向协议,并让我们对类感到羞耻,但 Swift 的实际设计恰恰相反。确保您避免上课,因为这些是真正的价值类型,而不仅仅是出于同伴压力。不要仅仅因为它是 Swift 就使用协议。我发现 PAT 是满足非常特殊需求(如 Collection)的工具,而不是大多数问题的首选解决方案。 (在 Swift 4 之前,甚至 Collection 都是一团糟的协议。)

【讨论】:

    猜你喜欢
    • 2011-11-27
    • 1970-01-01
    • 2022-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多