4

私は、構造体で、型が実行時に次のように定義される汎用プロパティを持つ方法を探していました。

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

これが役立つ単純な使用例は、jsonオブジェクトを構築する場合です。Anodeは 、 、 、配列などにすることができますがintstring変更bool可能な型を除けば、オブジェクトnodeは同じままです。

少し考えて使用に失敗した後protocols(通常のエラーが発生しました)、2 つの異なる解決策をprotocol 'X' can only be used as a generic constraint because it has Self or associated type requirements思いつきましtype erasureた。type-erasuregenerics

#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: オプション値

ロブのすばらしい答えを使用して、次の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タイプが に与えられた場合、次の結果を与えるロブの答えを使用する可能性を探していますvalue:

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

編集 #1 2020/02/08: 解決策

valueさて、私は値を与えることに集中していたので、推論エラーの原因となる型がnilないことに気づきませんでした。nil

オプションのタイプを指定すると、機能します。

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

2 に答える 2