【问题标题】:enums with Associated Values + generics + protocol with associatedtype带有关联值的枚举 + 泛型 + 带有关联类型的协议
【发布时间】:2019-08-05 15:31:36
【问题描述】:

我正在尝试使我的 API 服务尽可能通用:

API 服务类

class ApiService {
  func send<T>(request: RestRequest) -> T {
    return request.parse()
  }
}

以便编译器可以从请求类别.auth.data 中推断出响应类型:

let apiService = ApiService()

// String
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
// Int
let intResponse = apiService.send(request: .data(.content(id: "123")))

我试图提出一个解决方案,使用泛型和具有关联类型的协议来以干净的方式处理解析。但是,我无法以简单且类型安全的方式将请求案例与不同的响应类型相关联:

protocol Parseable {
  associatedtype ResponseType
  func parse() -> ResponseType
}

端点

enum RestRequest {

  case auth(_ request: AuthRequest)
  case data(_ request: DataRequest)

  // COMPILER ERROR HERE: Generic parameter 'T' is not used in function signature
  func parse<T: Parseable>() -> T.ResponseType {
    switch self {
    case .auth(let request): return (request as T).parse()
    case .data(let request): return (request as T).parse()
    }
  }

  enum AuthRequest: Parseable {
    case login(email: String, password: String)
    case signupWithFacebook(token: String)

    typealias ResponseType = String
    func parse() -> ResponseType {
        return "String!!!"
    }
  }
  enum DataRequest: Parseable {
    case content(id: String?)
    case package(id: String?)

    typealias ResponseType = Int
    func parse() -> ResponseType {
        return 16
    }
  }
}

即使我使用T.ResponseType 作为函数返回,T 怎么没有在函数签名中使用?

有没有更好更干净的方法来实现这一点?

【问题讨论】:

    标签: swift generics protocols associated-types


    【解决方案1】:

    我正在尝试使我的 API 服务尽可能通用:

    首先,最重要的是,这绝不应该是一个目标。相反,您应该从用例开始,并确保您的 API 服务满足它们。 “尽可能通用”没有任何意义,只会让您在向事物添加“通用功能”时陷入类型噩梦,这与对许多用例普遍有用的东西不同。哪些调用者需要这种灵活性?从调用者开始,协议将随之而来。

    func send<T>(request: RestRequest) -> T
    

    接下来,这是一个非常糟糕的签名。您不希望对返回类型进行类型推断。管理是一场噩梦。相反,在 Swift 中执行此操作的标准方法是:

    func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType
    

    通过将预期结果类型作为参数传递,您可以摆脱类型推断的麻烦。头痛是这样的:

    let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
    

    编译器如何知道stringResponse 应该是一个字符串?这里没有说“字符串”。所以你必须这样做:

    let stringResponse: String = ...
    

    那是非常丑陋的 Swift。相反,您可能想要(但不是真的):

    let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
                                         returning: String.self)
    

    “但不是真的”,因为没有办法很好地实现这一点。 send 怎么知道如何将“我得到的任何响应”翻译成“碰巧称为 String 的未知类型”?那会做什么?

    protocol Parseable {
      associatedtype ResponseType
      func parse() -> ResponseType
    }
    

    这个 PAT(带有关联类型的协议)实际上没有意义。它说如果某个实例可以返回 ResponseType,则某些内容是可解析的。但这将是一个 解析器 而不是“可以解析的东西”。

    对于可以解析的内容,您需要一个可以接受一些输入并自行创建的 init。最好的通常是 Codable,但您可以自己制作,例如:

    protocol Parseable {
        init(parsing data: Data) throws
    }
    

    但我倾向于 Codable,或者只是传递解析函数(见下文)。

    enum RestRequest {}
    

    这可能是对枚举的不好使用,特别是如果您正在寻找的是一般可用性。每个新的 RestRequest 都需要更新 parse,这是这种代码的错误位置。枚举使添加新的“所有实例都实现的东西”变得容易,但很难添加“新类型的实例”。结构(+协议)是相反的。它们使添加新的协议种类变得容易,但很难添加新的协议要求。请求,尤其是在通用系统中,属于后一种类型。您想一直添加新请求。枚举使这变得困难。

    有没有更好更干净的方法来实现这一点?

    这取决于“这个”是什么。你的调用代码是什么样的?您当前的系统在哪里创建了您想要消除的代码重复?你的用例是什么?没有“尽可能通用”这样的东西。只有一些系统可以沿着他们准备处理的轴适应用例。不同的配置轴导致不同种类的多态性,并有不同的权衡。

    你希望你的调用代码是什么样的?

    只是为了提供一个例子说明这可能是什么样子,不过,它应该是这样的。

    final class ApiService {
        let urlSession: URLSession
        init(urlSession: URLSession = .shared) {
            self.urlSession = urlSession
        }
    
        func send<Response: Decodable>(request: URLRequest,
                                       returning: Response.Type,
                                       completion: @escaping (Response?) -> Void) {
            urlSession.dataTask(with: request) { (data, response, error) in
                if let error = error {
                    // Log your error
                    completion(nil)
                    return
                }
    
                if let data = data {
                    let result = try? JSONDecoder().decode(Response.self, from: data)
                    // Probably check for nil here and log an error
                    completion(result)
                    return
                }
                // Probably log an error
                completion(nil)
            }
        }
    }
    

    这是非常通用的,可以应用于多种用例(尽管这种特殊形式非常原始)。您可能会发现它并不适用于您的所有用例,因此您将开始对其进行扩展。例如,也许你不喜欢在这里使用 Decodable。你想要一个更通用的解析器。没关系,让解析器可配置:

    func send<Response>(request: URLRequest,
                        returning: Response.Type,
                        parsedBy: @escaping (Data) -> Response?,
                        completion: @escaping (Response?) -> Void) {
    
        urlSession.dataTask(with: request) { (data, response, error) in
            if let error = error {
                // Log your error
                completion(nil)
                return
            }
    
            if let data = data {
                let result = parsedBy(data)
                // Probably check for nil here and log an error
                completion(result)
                return
            }
            // Probably log an error
            completion(nil)
        }
    }
    

    也许你想要两种方法。没关系,在另一个之上构建一个:

    func send<Response: Decodable>(request: URLRequest,
                                   returning: Response.Type,
                                   completion: @escaping (Response?) -> Void) {
        send(request: request,
             returning: returning,
             parsedBy: { try? JSONDecoder().decode(Response.self, from: $0) },
             completion: completion)
    }
    

    如果您正在寻找有关此主题的更多信息,您可能会对"Beyond Crusty" 感兴趣,其中包括一个将您正在讨论的解析器捆绑在一起的示例。有点过时了,现在 Swift 协议更强大了,但基本消息没有改变,在这个例子中是 parsedBy 之类的基础。

    【讨论】:

    • 我理解您的建议,但有一些限制(过去的错误决定和非常大的遗留代码库)不允许我以更简洁的方式重新编写整个 API(至少不是现在)。相信我,我自己也不会走这条“尽可能通用”的道路。不过还是谢谢你的详细回答!非常感谢!
    • 很久没有找到这么详尽且写得很好的答案了,Rob 干得好!
    猜你喜欢
    • 1970-01-01
    • 2020-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-13
    相关资源
    最近更新 更多