如何在 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())
}
}
呃,其实这个有效!
我在跟你开玩笑吗?号
我对这个不太满意,因为PipelineImpl
的initializer
很通用,所以我希望它在协议中(我这个痴迷是不是错了?)。这导致了两端:
协议 Pipeline
将是通用的。 initializer
包含一个引用 placeholder T
的 where 子句,因此我需要将 placeholder T
作为 associated type
移动到协议中。然后协议变成通用协议,这意味着我不能在我的客户端代码中直接使用它——可能需要另一种类型的擦除。
虽然我可以忍受为Pipeline
协议写另一个类型擦除的麻烦,但我不知道如何处理initializer function
,因为AnyPipeline<T>
class 必须实现关于协议的初始化器,但它实际上只是一个 thunk class,它本身不应该实现任何初始化器。
保持协议 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()
。因此,这也意味着(至少现在)您不需要 AnySource
或 AnyProcedure
类型的擦除。
这个包装器的一个简单实现是:
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)
}
}
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())
}
}
呃,其实这个有效! 我在跟你开玩笑吗?号
我对这个不太满意,因为PipelineImpl
的initializer
很通用,所以我希望它在协议中(我这个痴迷是不是错了?)。这导致了两端:
协议
Pipeline
将是通用的。initializer
包含一个引用placeholder T
的 where 子句,因此我需要将placeholder T
作为associated type
移动到协议中。然后协议变成通用协议,这意味着我不能在我的客户端代码中直接使用它——可能需要另一种类型的擦除。虽然我可以忍受为
Pipeline
协议写另一个类型擦除的麻烦,但我不知道如何处理initializer function
,因为AnyPipeline<T>
class 必须实现关于协议的初始化器,但它实际上只是一个 thunk class,它本身不应该实现任何初始化器。保持协议
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()
。因此,这也意味着(至少现在)您不需要 AnySource
或 AnyProcedure
类型的擦除。
这个包装器的一个简单实现是:
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)
}
}