Swift:嵌套类型擦除

时间:2017-07-08 15:20:11

标签: ios swift swift3 type-erasure

使用Swift 3.0(我可以使用Swift 4.0,如果这对我有帮助......但我不认为它会这样)我想要清除两个级别。我要键入什么来擦除具有相关类型的协议,该协议符合协议本身又具有相关类型的协议。所以可以说我想键入erase 嵌套关联类型。

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

原始场景 - 未解决

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可以包含任何protocol Vehicle { var speed: Int { get } } protocol Transportation { associatedtype Transport: Vehicle var transport: Transport { get } var name: String { get } }

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

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

简化场景 - 未解决

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

Bus

然后我们假设Vehicle符合struct Bus: Vehicle { var speed: Int { return 60 } }

RedBusLine

然后我们有两个不同的 BusLines BlueBusLineTransportation都符合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 } }

Transportation

然后我们可以使用base和box模式和类键入erase 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") } } ,如bignerdranch here所述:

RedBusLine

然后,我们可以将BlueBusLinelet busRides: [AnyTransportation<Bus>] = [AnyTransportation(RedBusLine()), AnyTransportation(BlueBusLine())] busRides.forEach { print($0.name) } // prints "Red line\nBlue line" 放入

Homogeneous Requirement

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

想象一下,我们有另一个Ferry,例如FerryLinestruct Ferry: Vehicle { var speed: Int { return 40 } } struct FerryLine: Transportation { let transport: Ferry = Ferry() var name = "Ferry line" }

Vehicle

我想我们现在要输入erase AnyTransportation<AnyVehicle>?因为我们想要一个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的类型,但[AnyTransportation<AnyVehicle>]当然不符合它。

但我无法找到解决方案。有没有?

问题1:是否可以键入擦除简单场景,允许:Transportation

问题2:如果简单场景是可解决的,原始场景是否也可以解决?

下面仅详细解释我想用原始场景

实现的目标

原始场景 - 扩展

我最初的需要是将任何Vehicle放在同一个数组中,其中Motor本身有任何let transportations: [AnyTransportation<AnyVehicle<AnyMotor>>] = [BusLine(), FerryLine()] // want to put `BusLine` and `FerryLine` in same array

@Override
public void initialize(@Nullable Bootstrap<PetalsCockpitConfiguration> bootstrap) {
    assert bootstrap != null;

    super.initialize(bootstrap);

    // TODO this is not the best because every new prefix must be added... if not, the static asset servlet will
    // take over instead of returning index.html
    // Improve when https://github.com/palantir/dropwizard-index-page/issues/38 is fixed
    bootstrap.addBundle(new IndexPageBundle("frontend/index.html",
            ImmutableSet.of("/", "/index.html", "/setup", "/login", "/workspaces", "/workspaces/*")));
    // no index file parameter because index is served by IndexPageBundle
    bootstrap.addBundle(new AssetsBundle("/frontend", "/", null));
}

2 个答案:

答案 0 :(得分:4)

如果您想用任何带有任何引擎的车辆表达任何交通工具,那么您需要3个箱子,每个箱子都按照“之前”类型擦除的包装纸进行交谈。您不希望在任何这些框上使用通用占位符,因为您希望根据完全异构的实例进行讨论(例如,不是任何具有特定 <div class="col-md-12"> <div class="form-group"> @Html.LabelFor(model => model.DescriptionAr, new { @class = "control-label" }) @Html.ValidationMessageFor(model => model.DescriptionAr) @Html.TextAreaFor(model => model.DescriptionAr, 10, 10, new { @class = "ckeditor form-control", dir = "rtl" }) </div> </div> 类型的运输,或任何带有特定的 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 没有任何关联类型,我们仍然构建了AnyMotor。这是因为bug report,所以我们不能使用Motor本身来满足Motor相关类型(需要Engine) - 我们目前必须为它构建一个具体的包装类型

答案 1 :(得分:3)

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都是一个协议的混乱。)