jul*_*adi 4 generics protocols associated-types swift
我试图使我的 API 服务尽可能通用:
API服务类
class ApiService {
func send<T>(request: RestRequest) -> T {
return request.parse()
}
}
Run Code Online (Sandbox Code Playgroud)
这样编译器就可以从请求类别中推断出响应类型,.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")))
Run Code Online (Sandbox Code Playgroud)
我试图提出一个使用泛型和具有关联类型的协议的解决方案,以干净的方式处理解析。但是,我无法以简单且类型安全的方式将请求案例与不同的响应类型相关联:
protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}
Run Code Online (Sandbox Code Playgroud)
端点
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
}
}
}
Run Code Online (Sandbox Code Playgroud)
T即使我T.ResponseType用作函数返回,函数签名中如何不使用?
有没有更好更干净的方法来实现这一目标?
我试图使我的 API 服务尽可能通用:
首先,也是最重要的,这永远不应该是一个目标。相反,您应该从用例开始,并确保您的 API 服务满足它们。“尽可能通用”并不意味着什么,只会让您在向事物添加“通用功能”时陷入类型噩梦,这与对许多用例普遍有用的东西不同。哪些呼叫者需要这种灵活性?从呼叫者开始,协议将随之而来。
func send<T>(request: RestRequest) -> T
Run Code Online (Sandbox Code Playgroud)
其次,这是一个非常糟糕的签名。您不希望对返回类型进行类型推断。管理是一场噩梦。相反,在 Swift 中执行此操作的标准方法是:
func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType
Run Code Online (Sandbox Code Playgroud)
通过将预期结果类型作为参数传递,您可以摆脱类型推断的麻烦。头痛是这样的:
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
Run Code Online (Sandbox Code Playgroud)
编译器如何知道它stringResponse应该是一个字符串?这里没有说“字符串”。所以你必须这样做:
let stringResponse: String = ...
Run Code Online (Sandbox Code Playgroud)
这是非常丑陋的斯威夫特。相反,您可能想要(但不是真的):
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
returning: String.self)
Run Code Online (Sandbox Code Playgroud)
“但不是真的”,因为没有办法很好地实现这一点。怎么send知道如何将“我得到的任何响应”翻译成“碰巧称为 String 的未知类型”?那会做什么?
protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}
Run Code Online (Sandbox Code Playgroud)
这个 PAT(带有关联类型的协议)实际上没有意义。如果它的一个实例可以返回一个 ResponseType,它表示某些东西是可解析的。但这将是一个解析器,而不是“可以解析的东西”。
对于可以解析的内容,您需要一个可以接受一些输入并创建自己的 init。最好的通常是 Codable,但您可以自己制作,例如:
protocol Parseable {
init(parsing data: Data) throws
}
Run Code Online (Sandbox Code Playgroud)
但我倾向于 Codable,或者只是传递解析函数(见下文)。
enum RestRequest {}
Run Code Online (Sandbox Code Playgroud)
这可能是 enum 的一个错误用法,特别是如果您正在寻找的是一般可用性。每个新的 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)
}
}
}
Run Code Online (Sandbox Code Playgroud)
这是非常通用的,可以应用于多种用例(尽管这种特殊形式非常原始)。您可能会发现它不适用于您的所有用例,因此您将开始扩展它。例如,也许您不喜欢在这里使用 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)
}
}
Run Code Online (Sandbox Code Playgroud)
也许你想要这两种方法。没关系,在另一个之上构建一个:
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)
}
Run Code Online (Sandbox Code Playgroud)
如果您正在寻找有关此主题的更多信息,您可能对“Beyond Crusty”感兴趣,其中包括将您正在讨论的那种解析器结合在一起的已解决示例。它有点过时了,而且 Swift 协议现在更强大了,但是基本消息没有改变,就像parsedBy这个例子中的基础一样。
| 归档时间: |
|
| 查看次数: |
1173 次 |
| 最近记录: |