如何从 Swift 泛型函数中捕获参数
Posted
技术标签:
【中文标题】如何从 Swift 泛型函数中捕获参数【英文标题】:How to capture arguments from Swift generic function 【发布时间】:2020-12-17 23:51:34 【问题描述】:我有一个 Swift 协议,它定义了一个用于发出网络请求的通用函数。它看起来像这样:
protocol TaskManagerProtocol
func run<V>(
task: Task<V>,
completion: @escaping (Result<V, Error>) -> Void
)
这在生产代码中效果很好,让我可以发出接收不同类型结果的请求。到目前为止,一切顺利。
问题:对于单元测试,我想要一个捕获参数的测试间谍,尤其是闭包。间谍长这样:
class TaskManagerSpy<Value>: TaskManagerProtocol
var callCount = 0
var task: [Task<Value>] = []
var completion: [(Result<Value, Error>) -> Void] = []
func run<V>(
task: Task<V>,
completion: @escaping (Result<V, Error>) -> Void
)
guard V.self == Value.self else
fatalError("run<V> doesn't match init<Value>")
callCount += 1
self.task.append(task)
self.completion.append(completion)
我的意图是让测试代码实例化TaskManagerSpy<SomeType>
并将其注入到调用run<V>(task:completion:)
的被测系统中。但就目前而言,append()
调用无法编译:
Cannot convert value of type 'Task<V>' to expected argument type 'Task<Value>'
闭包也是如此。如果我注释掉这些行,测试就会成功,不会触发致命错误。
问题:我已经证明类型是相同的。有没有办法将一种类型强制转换为另一种类型,以便我可以捕获参数?
【问题讨论】:
【参考方案1】:既然如你所说,你已经断言V.self == Value.self
,你应该可以强制转换task
:
self.task.append(task as! Task<Value>)
completion
也一样:
self.completion.append(completion as! ((Result<Value, Error>) -> Void))
【讨论】:
乍一看,类型转换看起来很“奇怪”,但这仅用于测试,事实上 TaskManagerSpy 恰好符合协议,但必须做与实际完全不同的事情TaskManager 的实现,我相信这是一个可行的方法。但是,我仍然有感觉,有更好的方法:) 同意强制使“感觉”错误,这通常意味着绕过 SwiftLint 规则,但我可以自信地说这可能是“最佳”方法。泛型函数可能是 Swift 类型系统中最讨厌模拟的合约。 只是一点点评论,您可能希望使用带有消息而不是强制转换的 XCTUnwrap 来使测试更加干净和可读:)【参考方案2】:您可以通过在 TaskManagerProtocol 中使用 associatedtype
而不是在 func run
上使用泛型类型 V
来做到这一点:
protocol TaskManagerProtocol
associatedtype ValueType
func run(
task: Task<ValueType>,
completion: @escaping (Result<ValueType, Error>) -> Void
)
class TaskManagerSpy<Value>: TaskManagerProtocol
var callCount = 0
var task: [Task<Value>] = []
var completion: [(Result<Value, Error>) -> Void] = []
func run(
task: Task<Value>,
completion: @escaping (Result<Value, Error>) -> Void
)
callCount += 1
self.task.append(task)
self.completion.append(completion)
【讨论】:
我更喜欢这种方法,然而——这是一个很大的问题——这引发了如何重构现有代码的问题。可能有一个 TaskManager 实例处理所有不同的值类型V
。可能这实际上是可解码和/或可编码的 - 取决于这是该任务的“输入”还是“输出”。现在,在使协议成为“PAT”之后,需要有多个 TaskManager 实例(类型取决于V
),这可能会严重影响客户端代码。 V
类型也暴露给客户端,它不是实现细节。
这种方法意味着你不能再拥有TaskManagerProtocol
类型的任何属性而不使整个封闭类型成为通用的。测试之外的用法可能不是您想要的。以上是关于如何从 Swift 泛型函数中捕获参数的主要内容,如果未能解决你的问题,请参考以下文章
Swift - 在具有可选参数的泛型函数中以 Nil 作为参数
Type 应该采用啥协议来让泛型函数将任何数字类型作为 Swift 中的参数?