首页 > 解决方案 > 与擦除类型的可编码一致性?

问题描述

我正在尝试编写一个通用函数来解析几种不同的数据类型。

最初这种方法只适用于 Codable 类型,因此它的泛型类型受到约束<T: Codable>并且一切正常。不过现在,我正在尝试扩展它以检查返回类型是否为 Codable,并根据该检查相应地解析数据

func parse<T>(from data: Data) throws -> T? {

    switch T.self {
    case is Codable:

        // convince the compiler that T is Codable

        return try? JSONDecoder().decode(T.self, from: data)

    case is [String: Any].Type:

        return try JSONSerialization.jsonObject(with: data, options: []) as? T

    default:
        return nil
    }

}

所以你可以看到类型检查工作正常,但是一旦我检查了它,我就会坚持JSONDecoder().decode(:)接受T作为一种类型。Codable上面的代码没有编译,有错误

Cannot convert value of type 'T' (generic parameter of instance method 'parse(from:)') to expected argument type 'T' (generic parameter of instance method 'decode(_:from:)')In argument type 'T.Type', 'T' does not conform to expected type 'Decodable'

我尝试了许多铸造技术,比如let decodableT: <T & Decodable> = T.self等等,但都失败了——通常是基于Decodable协议和T具体类型的事实。

是否有可能(有条件地)将协议一致性重新引入像这样的已擦除类型?我很感激你有任何想法,无论是解决这种方法还是类似的通用解析方法,在这里可能会更成功。

编辑:一个并发症

@vadian 很有帮助地建议创建两种parse(:)具有不同类型约束的方法,以使用一个签名来处理所有情况。在许多情况下,这是一个很好的解决方案,如果您稍后遇到这个问题,它可能会很好地解决您的难题。

不幸的是,这仅在调用时已知类型的情况下才有效parse(:)-在我的应用程序中,此方法由另一个泛型方法调用,这意味着该类型已被擦除并且编译器无法选择正确约束parse(:)执行。

所以,要澄清这个问题的症结所在:是否可以有条件地/可选地将类型信息(例如协议一致性)添加已擦除的类型?换句话说,一旦一个类型变成了<T>,有什么办法可以把它转换成<T: Decodable>?

标签: swiftgenericscastingcodabletype-erasure

解决方案


您可以有条件T.self地转换为Decodable.Type以获得描述底层Decodable符合类型的元类型:

  switch T.self {
  case let decodableType as Decodable.Type:

但是,如果我们尝试传递decodableTypeJSONDecoder,我们就会遇到问题:

// error: Cannot invoke 'decode' with an argument list of type
// '(Decodable.Type, from: Data)'
return try? JSONDecoder().decode(decodableType, from: data)

这不起作用,因为decode(_:from:)有一个通用的占位符T : Decodable

func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T 

我们不能满足T.Type因为Decodable.TypeDecodable 符合它自己

正如在上面链接的问答中所探讨的,此问题的一种解决方法是打开Decodable.Type以挖掘出符合的底层具体类型Decodable——然后我们可以使用它来满足T.

这可以通过协议扩展来完成:

extension Decodable {
  static func openedJSONDecode(
    using decoder: JSONDecoder, from data: Data
  ) throws -> Self {
    return try decoder.decode(self, from: data)
  }
}

然后我们可以称之为:

return try? (decodableType.openedJSONDecode(using: JSONDecoder(), from: data) as! T)

你会注意到我们必须插入一个强制转换到T. 这是因为通过转换T.selfDecodable.Type我们消除了元类型也描述类型的事实T。因此,我们需要强制转换以获取此信息。

总而言之,你会希望你的函数看起来像这样:

func jsonDecode<T>(_ metatype: T.Type, from data: Data) throws -> T {
  switch metatype {
  case let codableType as Decodable.Type:
    let decoded = try codableType.openedJSONDecode(
      using: JSONDecoder(), from: data
    )

    // The force cast `as! T` is guaranteed to always succeed due to the fact
    // that we're decoding using `metatype: T.Type`. The cast to
    // `Decodable.Type` unfortunately erases this information.
    return decoded as! T

  case is [String: Any].Type, is [Any].Type:
    let rawDecoded = try JSONSerialization.jsonObject(with: data, options: [])
    guard let decoded = rawDecoded as? T else {
      throw DecodingError.typeMismatch(
        type(of: rawDecoded), .init(codingPath: [], debugDescription: """
        Expected to decode \(metatype), found \(type(of: rawDecoded)) instead.
        """)
      )
    }
    return decoded

  default:
    fatalError("\(metatype) is not Decodable nor [String: Any] nor [Any]")
  }
}

我做了一些其他的改变:

  • 将返回类型从 更改T?T。通常,您要么希望通过让函数返回一个可选项或让它抛出来处理错误——调用者同时处理这两种方式可能会非常混乱。

  • 为 .添加了显式参数T.Type。这避免了依赖调用者使用返回类型推断来满足,IMO与 API 设计指南不鼓励的T按返回类型重载的模式类似。

  • 提出这种default:情况fatalError,因为提供不可解码的类型可能是程序员的错误。


推荐阅读