草庐IT

swift - 带有关联值的枚举 + 泛型 + 带有关联类型的协议(protocol)

coder 2023-09-13 原文

我试图使我的 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)的解决方案,以干净的方式处理解析。但是,我无法以简单且类型安全的方式将请求案例与不同的响应类型相关联:
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即使我正在使用 T.ResponseType 也没有在函数签名中使用作为函数返回?

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

最佳答案

I'm trying to make my API Service as generic as possible:



首先,也是最重要的,这永远不应成为目标。相反,您应该从用例开始,并确保您的 API 服务满足它们。 “尽可能通用”并不意味着什么,只会让您在向事物添加“通用功能”时陷入类型噩梦,这与对许多用例普遍有用的东西不同。哪些调用者需要这种灵 active ?从调用者开始,协议(protocol)将随之而来。
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知道如何将“我得到的任何响应”翻译成“一种未知类型,恰好被称为字符串?”那会做什么?
protocol Parseable {
  associatedtype ResponseType
  func parse() -> ResponseType
}

这个 PAT(带有关联类型的协议(protocol))并没有真正的意义。如果它的一个实例可以返回一个 ResponseType,它表示某些东西是可解析的。但这将是一个解析器,而不是“可以解析的东西”。

对于可以解析的内容,您需要一个可以接受一些输入并创建自己的 init。最好的通常是 Codable,但您可以自己制作,例如:
protocol Parseable {
    init(parsing data: Data) throws
}

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

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

Is there a better still clean way to achieve this?



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

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

只是为了提供一个例子来说明这可能是什么样子,但它会是这样的。
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" 感兴趣。其中包括将您正在讨论的类型的解析器绑定(bind)在一起的已解决示例。它有点过时了,现在 Swift 协议(protocol)更强大了,但基本信息没有改变,像 parsedBy 之类的东西的基础。在这个例子中。

关于swift - 带有关联值的枚举 + 泛型 + 带有关联类型的协议(protocol),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55167586/

有关swift - 带有关联值的枚举 + 泛型 + 带有关联类型的协议(protocol)的更多相关文章

  1. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  2. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  3. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

  4. ruby-on-rails - 复数 for fields_for has_many 关联未显示在 View 中 - 2

    目前,Itembelongs_toCompany和has_manyItemVariants。我正在尝试使用嵌套的fields_for通过Item表单添加ItemVariant字段,但是使用:item_variants不显示该表单。只有当我使用单数时才会显示。我检查了我的关联,它们似乎是正确的,这可能与嵌套在公司下的项目有关,还是我遗漏了其他东西?提前致谢。注意:下面的代码片段中省略了不相关的代码。编辑:不知道这是否相关,但我正在使用CanCan进行身份验证。routes.rbresources:companiesdoresources:itemsenditem.rbclassItemi

  5. ruby - 使用 `+=` 和 `send` 方法 - 2

    如何将send与+=一起使用?a=20;a.send"+=",10undefinedmethod`+='for20:Fixnuma=20;a+=10=>30 最佳答案 恐怕你不能。+=不是方法,而是语法糖。参见http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_expressions.html它说Incommonwithmanyotherlanguages,Rubyhasasyntacticshortcut:a=a+2maybewrittenasa+=2.你能做的最好的事情是:

  6. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  7. ruby-on-rails - 带有 Zeus 的 RSpec 3.1,我应该在 spec_helper 中要求 'rspec/rails' 吗? - 2

    使用rspec-rails3.0+,测试设置分为spec_helper和rails_helper我注意到生成的spec_helper不需要'rspec/rails'。这会导致zeus崩溃:spec_helper.rb:5:in`':undefinedmethod`configure'forRSpec:Module(NoMethodError)对thisissue最常见的回应是需要'rspec/rails'。但这是否会破坏仅使用spec_helper拆分rails规范和PORO规范的全部目的?或者这无关紧要,因为Zeus无论如何都会预加载Rails?我应该在我的spec_helper中做

  8. ruby - 如何计算 Liquid 中的变量 +1 - 2

    我对如何计算通过{%assignvar=0%}赋值的变量加一完全感到困惑。这应该是最简单的任务。到目前为止,这是我尝试过的:{%assignamount=0%}{%forvariantinproduct.variants%}{%assignamount=amount+1%}{%endfor%}Amount:{{amount}}结果总是0。也许我忽略了一些明显的东西。也许有更好的方法。我想要存档的只是获取运行的迭代次数。 最佳答案 因为{{incrementamount}}将输出您的变量值并且不会影响{%assign%}定义的变量,我

  9. ruby-on-rails - Rails 中同一个类的多个关联的最佳实践? - 2

    我认为我的问题最好用一个例子来描述。假设我有一个名为“Thing”的简单模型,它有一些简单数据类型的属性。像...Thing-foo:string-goo:string-bar:int这并不难。数据库表将包含具有这三个属性的三列,我可以使用@thing.foo或@thing.bar之类的东西访问它们。但我要解决的问题是当“foo”或“goo”不再包含在简单数据类型中时会发生什么?假设foo和goo代表相同类型的对象。也就是说,它们都是“Whazit”的实例,只是数据不同。所以现在事情可能看起来像这样......Thing-bar:int但是现在有一个新的模型叫做“Whazit”,看起来

  10. Ruby:如何使用带有散列的 'send' 方法调用方法? - 2

    假设我有一个类A,里面有一些方法。假设stringmethodName是这些方法之一,我已经知道我想给它什么参数。它们在散列中{'param1'=>value1,'param2'=>value2}所以我有:params={'param1'=>value1,'param2'=>value2}a=A.new()a.send(methodName,value1,value2)#callmethodnamewithbothparams我希望能够通过传递我的哈希以某种方式调用该方法。这可能吗? 最佳答案 确保methodName是一个符号,而

随机推荐