【问题标题】:Using Generics in completionHandler在 completionHandler 中使用泛型
【发布时间】:2018-04-06 11:17:13
【问题描述】:

我有一个简单的应用程序,它使用自定义协议通过 TCP 套接字与服务器通信。我想实现类似 HTTP 的响应请求行为,从套接字层抽象出来。 所以我有简单的协议:

protocol ResponseType {
   init(with frame: SocketMessage)
}

还有一些例子:

struct MessageAck: ResponseType {
  var messageId: String
  init(with frame: SocketMessage) {
    messageId = frame.messageId
  }
}

我为发送请求创建了简单的协议:

protocol APIClient {
  func send<T: ResponseType>(request: SocketAPIRequest, completion:  ((Result<T>) -> Void)?)
}

enum SocketAPIRequest {
   case textMessage(messageId: String, ...)
   ...
}

最后:

enum Result<T> {
  case success(T)
  case failure(Error)
}

class SocketAPIClient: APIClient {
  typealias MessageId = String
  private var callbacks = [Receipt: ((Result<ResponseType>) -> Void)]()

  ...

  func send<T>(request: SocketAPIRequest, completion: ((Result<T>) -> Void)?) where T : ResponseType {
    ....
    callbacks[stompFrame.receiptId] = completion
    ....
  }
}

所以,当我想为每个请求存储回调,在收到应答后调用它,我得到了这样的错误:

Cannot assign value of type '((Result<T>) -> Void)?' to type '((Result<ResponseType>) -> Void)?'

我猜是混合类型和对象的问题,或者可能是别的什么。

【问题讨论】:

    标签: ios swift generics closures


    【解决方案1】:

    Swift 泛型不是协变的(对于涉及复制元素的 Array 有特殊的硬编码异常)。这意味着Result&lt;Apple&gt; 不是Result&lt;Fruit&gt; 的子类型。有关原因的示例,请参阅 Swift Generics & Upcasting

    在您的情况下,什么会阻止您将 Result&lt;MessageBody&gt; 传递给期望 Result&lt;MessageAck&gt; 的回调?例如:

    for callback in callbacks {
        callback(result)
    }
    

    对于任何给定类型的result,您怎么知道这在编译时是合法的?

    编辑(更好的答案):

    您可以将类型隐藏在闭包中以获得所需的内容。试试这个:

    class SocketAPIClient: APIClient {
        typealias MessageId = String
        private var callbacks = [Receipt: ((Result<SocketMessage>) -> Void)]() // <--- Change
    
        func send<T>(request: SocketAPIRequest, completion: ((Result<T>) -> Void)?) where T : ResponseType {
    
            // Store the closure we don't understand inside a closure we do
            callbacks[stompFrame.receiptId] = { result in
                switch result {
                case .success(let message):
                    completion?(.success(T.init(with: message)))
                case .failure(let error):
                    completion?(.failure(error))
                }
            }
        }
    }
    

    现在,与其尝试将T 直接保存在callbacks 中,不如将它保存在每个单独的闭包中,对类的其余部分隐藏,并且T 永远不会逃避这个函数。当您到达您在代码中调用callback 的任何位置时,只需将Result&lt;SocketMessage&gt; 传递给它,我假设您已经在某个地方。


    旧答案:

    解决问题的最简单方法是让回调始终传递 Result&lt;Data&gt; 并完全删除 T

    protocol APIClient {
        func send(request: SocketAPIRequest, completion: ((Result<Data>) -> Void)?)
    }
    

    然后将其留给MessageAck(在完成处理程序中)从原始数据中反序列化。

    还有其他方法可以使用类型橡皮擦来实现所有这些,但它们要复杂得多,有时也非常繁琐。

    【讨论】:

      【解决方案2】:

      你试过下面的签名吗

      func send<T:ResponseType>(request: SocketAPIRequest, completion: ((Result<T>) -> Void)?){ ... }
      

      仍然出现错误?

      编辑 1: 或者也许你应该尝试这样的事情

      protocol APIClient {
        associatedtype T
        func send(request: SocketAPIRequest, completion:  ((Result<T>) -> Void)?)
      }
      

      和,

      class SocketAPIClient: APIClient {
        typealias MessageId = String
        typealias T = ResponseType
        private var callbacks = [Receipt: ((Result<ResponseType>) -> Void)]()
      
        ...
      
        func send(request: SocketAPIRequest, completion: ((Result<T>) -> Void)?) {
          ....
          callbacks[stompFrame.receiptId] = completion
          ....
        }
      }
      

      【讨论】:

      • 好吧,这不对,但是将您的完成投射为? ((Result) -> Void)'。我在您的代码中没有看到任何错误(逻辑上)。
      • 代码不正确,因为Result&lt;MessageAck&gt; 不是Result&lt;ResponseType&gt; 的一种。它们在 Swift 中是完全不相关的类型。 (并非所有语言都如此,理论上可以添加这种协变,但它们在 Swift 中是不相关的类型。)
      • 您的编辑只是将ResponseType 重命名为T。它不再是通用的。该方法现在必须接受Result&lt;ResponseType&gt;,而不是Result&lt;MessageAck&gt;
      猜你喜欢
      • 2021-09-21
      • 1970-01-01
      • 2017-06-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-11
      • 1970-01-01
      相关资源
      最近更新 更多