Swift使用SignalR时存储Decodable回调的字典类型问题
我在Swift中使用SignalR,尝试实现不同方法的订阅功能。
根据官方文档说明:
需要注意的是,处理程序的参数类型必须明确指定,且与服务端发送的值类型兼容(例如服务端调用方法时传入字符串类型参数,处理程序的参数类型就不能是Int)。处理程序的参数数量需要与服务端调用客户端方法时传入的参数数量保持一致。
我仅知晓所有参数都遵循Decodable协议,因此编写了如下泛型函数来处理订阅逻辑:
private func subscribe<T: Decodable>(_ hubConnection: HubConnection, _ methodName: String, _ callback: @escaping ([T?])->()) { switch expectedParametersCount { case 2: hubConnection.on(method: methodName) { arg1, arg2 in self.addAction({ callback([arg1, arg2])}) } break } }
调用上述subscribe函数的代码如下:
private func hubConnSubscribe(_ hubConnection: HubConnection) { for kvPair in methodSubscriptions { subscribe(hubConnection, kvPair.key, kvPair.value) } }
问题出在回调的存储环节,我最初定义的存储字典如下:
private var methodSubscriptions = [String : ([Any?])->()]()
此时编译器抛出错误:
Protocol 'Any' as a type cannot conform to 'Decodable'
随后我将字典修改为如下形式:
[String : ([Decodable?])->()]()
编译器又抛出如下错误:
Protocol 'Decodable' as a type cannot conform to the protocol itself
你遇到的核心问题是Swift的协议存在*存在型(existential)*限制:直接把协议本身作为类型传入泛型约束时,协议类型自身并不遵循该协议,Decodable也不例外,因此无法满足subscribe函数要求的T: Decodable泛型约束。
正确的实现方式是做类型擦除,把不同Decodable类型的回调包装成统一的、不依赖泛型类型参数的闭包存储,不要直接在字典里存带泛型约束的闭包类型:
- 不要用
[Decodable?]作为回调参数,提前在注册回调的地方就做好类型绑定,闭包内部直接处理具体类型的解码逻辑 - 存储层统一存无泛型参数的
(HubConnection) -> Void类型的包装闭包,注册时就把具体类型的解码逻辑封装在闭包内部,避免跨层传递泛型参数
示例实现代码:
// 存储字典直接存绑定好具体类型的包装闭包,不需要带泛型 private var methodSubscriptions = [String: (HubConnection) -> Void]() // 注册订阅的方法,在调用时就明确具体的Decodable类型 func registerMethod<T: Decodable>(methodName: String, callback: @escaping ([T?]) -> Void) { // 把类型逻辑封装在闭包内部,存储时不需要暴露泛型 methodSubscriptions[methodName] = { hubConnection in // 可根据业务逻辑自行实现参数个数判断,也可按方法名提前配置参数数量 switch expectedParametersCount { case 2: hubConnection.on(method: methodName) { (arg1: T?, arg2: T?) in callback([arg1, arg2]) } default: break } } } // 统一订阅时直接调用存储的闭包即可,不需要处理泛型匹配 private func hubConnSubscribe(_ hubConnection: HubConnection) { methodSubscriptions.values.forEach { $0(hubConnection) } }
如果你需要动态接收未知结构的Decodable参数,不要直接用[Decodable?]承接,可以先把SignalR返回的原始JSON数据收下来,再根据业务需要解码成对应的具体类型:
- SignalR-Client-Swift本身支持接收原始
JSON类型参数,你可以统一把回调参数类型设为JSON,在回调内部再做JSONDecoder解码,完全绕开泛型存储的类型约束问题 - 不要尝试把存在型的
Decodable作为泛型参数传入,Swift的类型系统不支持这种用法,类型擦除是这类场景的标准处理方案
内容的提问来源于stack exchange,提问作者Gama

