如何从 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&lt;SomeType&gt; 并将其注入到调用run&lt;V&gt;(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&lt;Value&gt;)

completion 也一样:

self.completion.append(completion as! ((Result&lt;Value, Error&gt;) -&gt; 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中泛型和 Any 类型

Swift - 在具有可选参数的泛型函数中以 Nil 作为参数

Type 应该采用啥协议来让泛型函数将任何数字类型作为 Swift 中的参数?

如何从泛型函数中记录参数[重复]

C++ STL学习 —— 模板泛型算法函数对象lambda 表达式(参数捕获)函数适配器

C++ STL学习 —— 模板泛型算法函数对象lambda 表达式(参数捕获)函数适配器