我编写了一个简单的FETCH客户端来处理请求.代码如下:

请求

public protocol 请求Parameter: Encodable {
  var queryItems: [URLQueryItem] { get }
}

public extension 请求Parameter {
  var queryItems: [URLQueryItem] {
    var this = [URLQueryItem]()
    let mirror = Mirror(reflecting: self)
    for child in mirror.children {
      guard let label = child.label, let value = child.value as? CustomStringConvertible else {
        continue
      }
      this.append(.init(name: label, value: value.description))
    }
    return this
  }
}

extension Dictionary: 请求Parameter where Key == String, Value: CustomStringConvertible & Encodable {
  public var queryItems: [URLQueryItem] {
    var this = [URLQueryItem]()
    for (key, value) in self {
      this.append(.init(name: key, value: value.description))
    }
    return this
  }
}

public protocol 请求<Parameter, Response> {
  associatedtype Parameter: 请求Parameter
  associatedtype Response: Decodable

  var method: HTTPMethod { get }
  var path: String { get }
  var parameter: Parameter? { get }
  var headers: HTTPHeaders? { get }
}

public extension 请求 {
  func build(_ urlString: String, encoder: JSONEncoder? = nil) -> URL请求 {
    var components = URLComponents(string: urlString)
    components?.path = path

    if method == .get {
      components?.queryItems = parameter?.queryItems
    }

    guard let url = components?.url else {
      fatalError("url can't be nil")
    }

    var url请求 = URL请求(url: url)
    url请求.httpMethod = method.rawValue

    if let headers {
      url请求.headers = headers
    }

    if method == .post, let parameter, let encoder {
      do {
        url请求.httpBody = try encoder.encode(parameter)
      } catch {
        fatalError("`parameter cannot be encoded`")
      }
    }

    return url请求
  }
}

客户端

public protocol 客户端 {
  func send<请求Type: 请求>(_ request: 请求Type, encoder: JSONEncoder?, decoder: JSONDecoder) async throws -> 请求Type.Response

  var urlString: String { get }

  static var shared: Self { get }
}

public extension 客户端 {
  func send<请求Type: 请求>(_ request: 请求Type, encoder: JSONEncoder? = nil, decoder: JSONDecoder = .init()) async throws -> 请求Type.Response {
    let (data, urlResponse) = try await URLSession.shared.data(for: request.build(urlString, encoder: encoder))
    guard let httpResponse = urlResponse as? HTTPURLResponse else {
      throw #InvalidResponse
    }
    guard 200 ... 299 ~= httpResponse.statusCode else {
      throw #NetworkFailure(httpResponse.statusCode)
    }
    return try decoder.decode(请求Type.Response.self, from: data)
  }
}

因此,一个简单且运行正常的网络请求代码如下所示:

struct Foo请求: 请求 {
  typealias Parameter = FooParameter
  typealias Response = Foo

  var method: HTTPMethod = .get
  var path: String = ""
  var parameter: FooParameter? = FooParameter()
  var headers: HTTPHeaders? = nil
}
struct FooParameter: 请求Parameter {}
struct Foo: Decodable {}
struct Foo客户端: 客户端 {
  var urlString: String = "fooURLString.com"

  static var shared: Foo客户端 = Self()
}

let result = try await Foo客户端.shared.send(Foo请求())

但有时一个简单的GET请求可能不需要参数.为了避免重复代码var parameter: FooParameter? = nilvar parameter: BarParameter? = nil,我添加了一个NeverParameter类型:

public struct NeverParameter: 请求Parameter {
  private init() {}
}

public extension 请求 where Parameter == NeverParameter {
  var parameter: Parameter? { nil }
}

在这一点上,问题出现了.我认为对于NeverParameter,请求应该是这样的:

struct Foo请求: 请求 {
  typealias Parameter = NeverParameter
  typealias Response = Foo

  var method: HTTPMethod = .get
  var path: String = ""
  var headers: HTTPHeaders? = nil
}

但实际上,注释掉typealias Parameter = NeverParameter仍然可以编译:

struct Foo请求: 请求 {
  // typealias Parameter = NeverParameter
  typealias Response = Foo

  var method: HTTPMethod = .get
  var path: String = ""
  var headers: HTTPHeaders? = nil
}

(lldb) po type(of: parameter)
Swift.Optional<请求.NeverParameter>

为什么编译器可以推断参数的类型是NeverParameter?即使只在WHERE子句NeverParameter下添加了缺省值nil? 并添加了具有相同实现的另一个NeverParameter1,Xcode最终抛出了一个错误:

public struct NeverParameter: 请求Parameter {
  private init() {}
}

public extension 请求 where Parameter == NeverParameter {
  var parameter: Parameter? { nil }
}

public struct NeverParameter1: 请求Parameter {
  private init() {}
}

public extension 请求 where Parameter == NeverParameter1 {
  var parameter: Parameter? { nil }
}

错误:

Type 'Foo请求' does not conform to protocol '请求'

推荐答案

关联类型推理在语言中已经存在了很长一段时间.一个微不足道的 case 是:

protocol Foo {
    associatedtype T
    func methodThatReturnsT() -> T
}

class Bar: Foo {
    func methodThatReturnsT() -> Int { 1 }
}

这不一定是编译器的实现方式,但您可以想象编译器的 idea :

Bar没有T类型.哦,但是methodThatReturnsT应该返回T.Bar.methodThatReturnsT返回Int,因此T必须是Int.

这里的情况基本上是相同的,只是直观程度降低了little,因为它涉及有条件的扩展.这种行为有is found to be rather "magical" by some people种,甚至有reported as a bug种.然而,这是预期中的行为.

这不一定是编译器的实现方式,但您可以想象编译器的 idea :

FooRequest没有parameter的声明.哦,但是它是由分机extension Request where Parameter == NeverParameter实现的.让我们使用在那里声明的parameter.

FooRequest也没有Parameter类型,但是parameter应该是Parameter类型.FooRequest.parameter(来自扩展)的类型为NeverParameter,因此T必须为NeverParameter.


这种关联的类型推断使得实现一些标准库协议比其他方式容易得多.例如,考虑一个OptionSet的典型实现.

struct Options: OptionSet {
    let rawValue: Int
    
    static let option1 = Options(rawValue: 1)
    static let option2 = Options(rawValue: 2)
    static let option3 = Options(rawValue: 4)
}

这里涉及哪些关联类型?OptionSet继承自RawRepresentableSetAlgebra,而SetAlgebra继承自ExpressibleByArrayLiteral,所以有:

  • RawValue
  • Element
  • ArrayLiteralElement

RawValue个是基于声明的rawValue属性推断的,这是直观的.OptionSetSelf声明为Element个的默认值,所以这不成问题.ArrayLiteralElement个是如何推断出来的?没有声明init(arrayLiteral:)个初始值设定项,因此不能由此推断.

这与您的FooRequest所在的情况相同-SetAlgebraconditional extension帮助推断ArrayLiteralElement个与Element个相同,而该扩展提供了init(arrayLiteral:)的默认实现.

extension SetAlgebra where Element == ArrayLiteralElement {
    public init(arrayLiteral: Element...) { ... }
}

请参阅SWIFT源代码中的extension declaration,或参阅该默认实施here的文档.

Swift相关问答推荐

动画过程中的SwiftUI重绘视图

SwiftUI同心圆,通过逐渐出现来显示进度

快速并行读取进程 standardOutput 和 standardError 而不会阻塞

ToolbarItem 包含在动画中但不应该包含在动画中

BLE 在 ESP32 和 Iphone 之间发送字符串

SwiftUI路径平滑连接两条线

允许为 self 分配新的 struct 值的理由是什么?

Swiftui 无法从核心数据中获取数据

如果 Swift 无法分配内存会怎样?

Swift 中的Combine和didSet有什么区别?

SwiftUI 视图的默认成员初始化器 VS 自定义初始化器

创建 ViewModel 时 iOS 模拟器上的黑屏

对齐时如何避免图像尺寸缩小

SwiftUI 中的计时器崩溃屏幕

快速的 AES 加密

为什么 swift 中的类没有存储类型属性?

为 UIActivityViewController Swift 设置不同的活动项

根据禁用与否更改 SwiftUI 中按钮的 colored颜色

将活动指示器 colored颜色 更改为黑色

ld:入口点(_main)未定义.对于架构 x86_64:Xcode 9