【问题标题】:Decoding a class based on encoded value Swift基于编码值Swift解码类
【发布时间】:2020-12-31 13:22:41
【问题描述】:

我正在尝试根据编码数据的内容解码特定类。

class Vehicle: Codable {
    enum Kind: Int, Codable {
        case car = 0, motorcycle = 1
    }
    
    let brand: String
    let numberOfWheels: Int
}

class Car: Vehicle {}
class MotorCycle: Vehicle {}

如您所见,我有一个通用的Vehicle 类型,用于对车辆进行编码和解码。这适用于如下所示的基本解码。

let car = "{\"kind\": 0, \"brand\": \"Ford\", \"number_of_wheels\": 4}".data(using: .utf8)!
let motorCycle = "{\"kind\": 1, \"brand\": \"Yamaha\", \"number_of_wheels\": 2}".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

// Outputs a Project.Car
let ford = try! decoder.decode(Car.self, from: car)
// Outputs a Project.MotorCycle
let yamaha = try! decoder.decode(MotorCycle.self, from: motorCycle)

但是,如果我想解码一组车辆,但将它们解码为特定类型,该怎么办?

let combined = "[{\"kind\": 0, \"brand\": \"Ford\", \"number_of_wheels\": 4}, {\"kind\": 1, \"brand\": \"Yamaha\", \"number_of_wheels\": 2}]".data(using: .utf8)!

// Outputs [Project.Vehicle, Project.Vehicle]
print(try! decoder.decode([Vehicle].self, from: combined))

如何使用 JSON 数据中的 kind 属性让解码器输出一组车辆,但输入车辆类型。如果可能的话,按照示例[Project.Car, Project.MotorCycle]

【问题讨论】:

  • 也许是挑剔,但 Vehicle 不是通用类而是超类
  • 是的,我的错。确实是超类
  • IMO 你应该直接删除子类,除非你有很好的理由这样做。

标签: json swift decoding codable


【解决方案1】:

这是不使用Codable 的替代解决方案。此外,我做了一些更改并引入了协议而不是超类。

protocol Vehicle: CustomStringConvertible {
    var brand: String { get set }
    var numberOfWheels: Int { get set }
}

extension Vehicle {
    var description: String {
        "\(brand), wheels: \(numberOfWheels), type: \(type(of:self))"
    }
}

不是很重要,但我将类型从类更改为结构

struct Car: Vehicle {
    var brand: String
    var numberOfWheels: Int
}

struct MotorCycle: Vehicle {
    var brand: String
    var numberOfWheels: Int
}

然后使用JSONSerialization进行解码,然后使用reduce(into:)创建对象,分两步将json转换为Vehicle数组

do {
    if let array = try JSONSerialization.jsonObject(with: combined) as? [[String: Any]] {
        let vehicles = array.reduce(into: [Vehicle]()) {
            if let kindValue = $1["kind"] as? Int,
               let kind = VehicleKind(rawValue: kindValue),
               let brand = $1["brand"] as? String,
               let numberOfWheels = $1["number_of_wheels"] as? Int {
                switch kind {
                case .car:
                    $0.append(Car(brand: brand, numberOfWheels: numberOfWheels))
                case .motorcycle:
                    $0.append(MotorCycle(brand: brand, numberOfWheels: numberOfWheels))
                }
            }
        }
        for vehicle in vehicles {
            print(vehicle)
        }
    }
} catch {
    print(error)
}

以上代码输出:

福特,车轮:4,类型:汽车
雅马哈,轮子:2,类型:摩托车


更新。可编码版本

我还设法提出了一个 Codable 解决方案,方法是引入一个单独的类型用于解码。设置与以前相同,带有一个协议和两个结构。

然后我介绍一种特定的解码类型(当然它也可以扩展为编码),它实现了自定义init(from:)

struct JsonVehicle: Decodable {
    let vehicle: Vehicle

    enum CodingKeys: String, CodingKey {
        case kind
        case brand
        case numberOfWheels
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let kind = try container.decode(VehicleKind.self, forKey: .kind)
        let brand = try container.decode(String.self, forKey: .brand)
        let wheels = try container.decode(Int.self, forKey: .numberOfWheels)
        switch kind {
        case .car:
            vehicle = Car(brand: brand, numberOfWheels: wheels)
        case .motorcycle:
            vehicle = MotorCycle(brand: brand, numberOfWheels: wheels)
        }
    }
}

最终结果还是分两步实现的

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    let result = try decoder.decode([JsonVehicle].self, from: combined)
    let vehicles = result.map(\.vehicle)
} catch {
    print(error)
}

【讨论】:

    【解决方案2】:

    你没有在类中定义 kind 属性。 另外,汽车和摩托车没有区别 我在操场上对此进行了测试,似乎可以满足您的要求。

        class Vehicle: Codable {
            enum Kind: Int, Codable {
                case car = 0, motorcycle = 1
            }
            let kind: Kind
            let brand: String
            let numberOfWheels: Int
        }
        
        class Car: Vehicle {}
        class MotorCycle: Vehicle {}
    
        let car = "{\"kind\": 0, \"brand\": \"Ford\", \"number_of_wheels\": 4}".data(using: .utf8)!
        let motorCycle = "{\"kind\": 1, \"brand\": \"Yamaha\", \"number_of_wheels\": 2}".data(using: .utf8)!
        
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        
        // Outputs a Project.Car
        let ford = try! decoder.decode(Car.self, from: car)
        // Outputs a Project.MotorCycle
        let yamaha = try! decoder.decode(MotorCycle.self, from: motorCycle)
        //But what if I want to decode an array of vehicles, but have them decoded as a specific type?
        
        let combined = "[{\"kind\": 0, \"brand\": \"Ford\", \"number_of_wheels\": 4}, {\"kind\": 1, \"brand\": \"Yamaha\", \"number_of_wheels\": 2}]".data(using: .utf8)!
        
        // Outputs [Project.Vehicle, Project.Vehicle]
        let allVeh = try! decoder.decode([Vehicle].self, from: combined)
        for veh in allVeh where veh.kind == Vehicle.Kind.motorcycle { // } is MotorCycle {
            print(veh.brand, type(of: veh))
        }
    

    它给出: 雅马哈汽车

    你也可以写

    let allMotorcycles = allVeh.filter {$0.kind == Vehicle.Kind.motorcycle}
    for moto in allMotorcycles  {
        print(moto.brand, type(of: moto))
    }
    

    【讨论】:

    • 我认为问题的重点是kind 包含为属性,并且仅使用它来确定要实例化的子类
    • Joakim 所说的确实如此。 JSON 中需要类型,但对象中不需要,因为类描述了类型。至于两个对象没有区别,那只是因为它是一个例子。实际上,这些问题是指一个异构数组,其中每个元素都有一个共同的超类。您的示例不允许根据请求转换为 MotorCycle 类型
    猜你喜欢
    • 1970-01-01
    • 2015-05-03
    • 1970-01-01
    • 2021-01-11
    • 1970-01-01
    • 1970-01-01
    • 2019-01-06
    • 2011-01-14
    • 2017-10-28
    相关资源
    最近更新 更多