struct со свойством generi c, соответствующим Encodable в Swift - PullRequest
2 голосов
/ 06 февраля 2020

Я искал в структуре способ иметь свойство generi c, где тип определяется во время выполнения, например:

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

Простой вариант использования, где это может быть полезно это при создании объекта json. node может быть int, string, bool, массивом и т. Д. c., Но кроме типа, который может изменяться, объект node остается прежним.

Подумав немного и потерпев неудачу с использованием protocols (получил обычную ошибку protocol 'X' can only be used as a generic constraint because it has Self or associated type requirements), я нашел 2 разных решения, # 0 с использованием type erasure и # 1 с использованием type-erasure и 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}]

Даже если результат идентичен, я твердо верю, что подход # 1 - это больше scalable, если учитывается больше типов, но все еще есть изменения должно быть сделано в 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

Я ищу возможность использовать ответ Роба, дающий следующий результат, если для value задан тип Optional:

[{"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),
]

Ответы [ 2 ]

3 голосов
/ 06 февраля 2020

Если то, что вы описали, действительно то, что вы хотите, это можно сделать без любого из этих типов ластиков. Все, что вам нужно, это закрытие. (Но это предполагает, что 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, остальному миру не нужно знать его тип (собаке даже не нужно знать его тип). Это то, что тип-стирание это все о. Для этого не требуется создавать дополнительные типы-обертки или обобщенные c структуры.

Если вы хотите сохранить такие типы, как 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)
0 голосов
/ 08 февраля 2020

Вот еще одно решение, которое может помочь:

struct Dog<V: Codable>: Codable {
   let id: String
   let value: V
}
...