首页 > 解决方案 > 具有通用属性的结构符合 Swift 中的 Encodable

问题描述

我一直在寻找一种在结构中具有通用属性的方法,该属性在运行时定义类型,例如:

struct Dog {
    let id: String
    let value: ??
}

一个可能有用的简单用例是在构建json对象时。Anode可以是int, string, bool, 数组等,但除了可以改变的类型之外,对象node保持不变。

经过一番思考并使用失败protocols(得到通常的protocol 'X' can only be used as a generic constraint because it has Self or associated type requirements错误)后,我想出了 2 个不同的解决方案,#0 usingtype erasure和 #1 using type-erasureand generics

#0(类型擦除)

struct AnyDog: Encodable {

    enum ValueType: Encodable {
        case int(Int)
        case string(String)

        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .int(let value):
                try container.encode(value)
            case .string(let value):
                try container.encode(value)
            }
        }
    }

    let id: String
    let value: ValueType

    init(_ dog: DogString) {
        self.id = dog.id
        self.value = .string(dog.value)
    }

    init(_ dog: DogInt) {
        self.id = dog.id
        self.value = .int(dog.value)
    }
}

struct DogString: Encodable{
    let id: String
    let value: String

    var toAny: AnyDog {
        return AnyDog(self)
    }
}

struct DogInt: Encodable {
    let id: String
    let value: Int

    var toAny: AnyDog {
        return AnyDog(self)
    }
}

let dogs: [AnyDog] = [
    DogString(id: "123", value: "pop").toAny,
    DogInt(id: "123", value: 123).toAny,
]

do {
    let data = try JSONEncoder().encode(dogs)
    print(String(data: data, encoding: .utf8)!)
} catch {
    print(error)
} 

#1(类型擦除+泛型)

struct AnyDog: Encodable {

    enum ValueType: Encodable {
        case int(Int)
        case string(String)

        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .int(let value):
                try container.encode(value)
            case .string(let value):
                try container.encode(value)
            }
        }
    }

    let id: String
    let value: ValueType
}

struct Dog<T: Encodable>: Encodable{
    let id: String
    let value: T

    var toAny: AnyDog {
        switch T.self {
        case is String.Type:
            return AnyDog(id: id, value: .string(value as! String))
        case is Int.Type:
            return AnyDog(id: id, value: .int(value as! Int))
        default:
            preconditionFailure("Invalid Type")
        }
    }
}
let dogs: [AnyDog] = [
    Dog<String>(id: "123", value: "pop").toAny ,
    Dog<Int>(id: "123", value: 123).toAny,
]

do {
    let data = try JSONEncoder().encode(dogs)
    print(String(data: data, encoding: .utf8)!)
} catch {
    print(error)
}

两种方法都给出了适当的结果:

[{"id":"123","value":"pop"},{"id":"123","value":123}]

即使结果相同,我坚信scalable如果考虑到更多类型,方法#1 更适用,但是对于添加的每种类型,仍然需要在 2 个不同的区域进行更改。

我确信有更好的方法来实现这一点,但还没有找到它。很高兴听到有关它的任何想法或建议。


编辑 #0 2020/02/08:可选值

使用 Rob 的最佳答案,我现在尝试允许value像这样可选:

struct Dog: Encodable {
    // This is the key to the solution: bury the type of value inside a closure
    let valueEncoder: (Encoder) throws -> Void

    init<T: Encodable>(id: String, value: T?) {
        self.valueEncoder = {
            var container = $0.container(keyedBy: CodingKeys.self)
            try container.encode(id, forKey: .id)
            try container.encode(value, forKey: .value)
        }
    }

    enum CodingKeys: String, CodingKey {
        case id, value
    }

    func encode(to encoder: Encoder) throws {
        try valueEncoder(encoder)
    }
}

let dogs = [
    Dog(id: "123", value: 123),
    Dog(id: "456", value: nil),
]

do {
    let data = try JSONEncoder().encode(dogs)
    print(String(data: data, encoding: .utf8)!)
} catch {
    print(error)
}

此时,T无法再推断,并抛出以下错误:

generic parameter 'T' could not be inferred

Optional如果给定类型,我正在寻找一种可能性,使用 Rob 的答案给出以下结果value

[{"id":"123","value":123},{"id":"456","value":null}]

编辑#1 2020/02/08:解决方案

好吧,我非常专注于给出我没有意识到没有任何类型导致推理错误的value值。nilnil

提供可选类型使其工作:

let optString: String? = nil
let dogs = [
    Dog(id: "123", value: 123),
    Dog(id: "456", value: optString),
]

标签: swiftgenericsstructtype-erasureencodable

解决方案


如果您所描述的确实是您想要的,则无需任何这些类型的橡皮擦即可完成。你所需要的只是一个闭包。(但这假设Dog真的只存在编码,正如你所描述的,除此之外没有什么需要value的。)

struct Dog: Encodable {
    // This is the key to the solution: bury the type of value inside a closure
    let valueEncoder: (Encoder) throws -> Void

    init<T: Encodable>(id: String, value: T) {
        self.valueEncoder = {
            var container = $0.container(keyedBy: CodingKeys.self)
            try container.encode(id, forKey: .id)
            try container.encode(value, forKey: .value)
        }
    }

    enum CodingKeys: String, CodingKey {
        case id, value
    }

    func encode(to encoder: Encoder) throws {
        try valueEncoder(encoder)
    }
}

因为value只在 内部使用valueEncoder,所以世界其他地方不需要知道它的类型(Dog 甚至不需要知道它的类型)。这就是类型擦除的全部内容。它不需要制作额外的包装类型或通用结构。

如果你想保留DogString和之类的类型DogInt,你也可以通过添加协议来做到这一点:

protocol Dog: Encodable {
    associatedtype Value: Encodable
    var id: String { get }
    var value: Value { get }
}

然后制作一个 DogEncoder 来处理编码(与上面相同,除了一个新的 init 方法):

struct DogEncoder: Encodable {
    let valueEncoder: (Encoder) throws -> Void

    init<D: Dog>(_ dog: D) {
        self.valueEncoder = {
            var container = $0.container(keyedBy: CodingKeys.self)
            try container.encode(dog.id, forKey: .id)
            try container.encode(dog.value, forKey: .value)
        }
    }

    enum CodingKeys: String, CodingKey {
        case id, value
    }

    func encode(to encoder: Encoder) throws {
        try valueEncoder(encoder)
    }
}

几种狗:

struct DogString: Dog {
    let id: String
    let value: String
}

struct DogInt: Dog  {
    let id: String
    let value: Int
}

将它们放入编码器数组中:

let dogs = [
    DogEncoder(DogString(id: "123", value: "pop")),
    DogEncoder(DogInt(id: "123", value: 123)),
]

let data = try JSONEncoder().encode(dogs)

推荐阅读