如何在 swift 中组合多个类型擦除模块?

How to compose multiple type-erased modules in swift?

Swift 很棒但还不成熟,所以有一些编译器限制,其中包括通用协议。出于类型安全方面的考虑,通用协议不能用作常规类型注释。我在 Hector Matos 的 post 中找到了解决方法。 Generic Protocols & Their Shortcomings

主要思想是使用类型擦除将通用协议转换为通用协议 class,这很酷。但是在将这项技术应用到更复杂的场景时,我遇到了困难。

假设有一个生成数据的抽象源,一个处理数据的抽象过程,以及一个将数据类型匹配的源和过程组合在一起的管道。

protocol Source {
    associatedtype DataType
    func newData() -> DataType
}
protocol Procedure {
    associatedtype DataType
    func process(data: DataType)
}
protocol Pipeline {
    func exec() // The execution may differ
}

我希望客户端代码简单如:

class Client {
    private let pipeline: Pipeline
    init(pipeline: Pipeline) {
        self.pipeline = pipeline
    }
    func work() {
        pipeline.exec()
    }
}

// Assume there are two implementation of Source and Procedure,
// SourceImpl and ProcedureImpl, whose DataType are identical.
// And also an implementation of Pipeline -- PipelineImpl

Client(pipeline: PipelineImpl(source: SourceImpl(), procedure: ProcedureImpl())).work()

实现 Source 和 Procedure 很简单,因为它们位于依赖项的底部:

class SourceImpl: Source {
    func newData() -> Int { return 1 }
}

class ProcedureImpl: Procedure {
    func process(data: Int) { print(data) }
}

执行流水线时出现 PITA

// A concrete Pipeline need to store the Source and Procedure, and they're generic protocols, so a type erasure is needed
class AnySource<T>: Source {
    private let _newData: () -> T
    required init<S: Source>(_ source: S) where S.DataType == T {
        _newData = source.newData
    } 
    func newData() -> T { return _newData() }
}
class AnyProcedure<T>: Procedure {
    // Similar to above.
}

class PipelineImpl<T>: Pipeline {
    private let source: AnySource<T>
    private let procedure: AnySource<T>
    required init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == T, P.DataType == T {
         self.source = AnySource(source)
         self.procedure = AnyProcedure(procedure)
    }
    func exec() {
         procedure.process(data: source.newData())
    }
}

呃,其实这个有效! 我在跟你开玩笑吗?号

我对这个不太满意,因为PipelineImplinitializer很通用,所以我希望它在协议中(我这个痴迷是不是错了?)。这导致了两端:

  1. 协议 Pipeline 将是通用的。 initializer 包含一个引用 placeholder T 的 where 子句,因此我需要将 placeholder T 作为 associated type 移动到协议中。然后协议变成通用协议,这意味着我不能在我的客户端代码中直接使用它——可能需要另一种类型的擦除。

    虽然我可以忍受为Pipeline协议写另一个类型擦除的麻烦,但我不知道如何处理initializer function,因为AnyPipeline<T> class 必须实现关于协议的初始化器,但它实际上只是一个 thunk class,它本身不应该实现任何初始化器。

  2. 保持协议 Pipeline 非通用。将 initializer 写成

    init<S: Source, P: Procedure>(source: S, procedure: P) 
    where S.DataType == P.DataType
    

    我可以防止协议被通用化。这意味着协议仅声明 "Source and Procedure must have same DataType and I don't care what it is"。这更有意义,但我未能实施具体的 class 确认此协议

    class PipelineImpl<T>: Protocol {
        private let source: AnySource<T>
        private let procedure: AnyProcedure<T>
        init<S: Source, P: Procedure>(source: S, procedure: P) 
        where S.DataType == P.DataType {
            self.source = AnySource(source) // doesn't compile, because S is nothing to do with T
            self.procedure = AnyProcedure(procedure) // doesn't compile as well
        }
        // If I add S.DataType == T, P.DataType == T condition to where clasue, 
        // the initializer won't confirm to the protocol and the compiler will complain as well
    }
    

那么,我该如何处理呢?

感谢阅读allll本文。

我认为你有点过于复杂了(除非我遗漏了什么)——你的 PipelineImpl 似乎只不过是一个函数的包装器,它从 Source 并将其传递给 Procedure.

因此,它不需要是通用的,因为外界不需要知道传递的数据类型——它只需要知道它可以调用 exec()。因此,这也意味着(至少现在)您不需要 AnySourceAnyProcedure 类型的擦除。

这个包装器的一个简单实现是:

struct PipelineImpl : Pipeline {

    private let _exec : () -> Void

    init<S : Source, P : Procedure>(source: S, procedure: P) where S.DataType == P.DataType {
        _exec = { procedure.process(data: source.newData()) }
    }

    func exec() {
        // do pre-work here (if any)
        _exec()
        // do post-work here (if any)
    }
}

这让您可以自由地将初始化程序添加到您的 Pipeline 协议中,因为它不需要关心实际的 DataType 是什么——只需要源和过程必须具有相同的 DataType:

protocol Pipeline {
    init<S : Source, P : Procedure>(source: S, procedure: P) where S.DataType == P.DataType
    func exec() // The execution may differ
}

@Hamish 指出了一个很好的解决方案。

发布这个问题后,我做了一些测试,也找到了解决方法

class PipelineImpl<T>: Pipeline {
    required init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == T, P.DataType == T {
        // This initializer does the real jobs.
        self.source = AnySource(source)
        self.procedure = AnyProcedure(procedure)
    }
    required convenience init<S: Source, P: Procedure>(source: S, procedure: P) where S.DataType == P.DataType {
        // This initializer confirms to the protocol and forwards the work to the initializer above
        self.init(source: source, procedure: procedure)
    }
}