Как вы декодируете известный конкретный тип, если этот конкретный тип хранится в переменной Decodable.Type во время выполнения? - PullRequest
0 голосов
/ 23 января 2020

Немного сбит с толку чем-то, что лучше всего иллюстрируется классом ...

class AnyDecodableWrapper : Decodable {

    static let decodableTypesLookup:[String:Decodable.Type] = [ <-- 'Decodable.Type' here is what's causing the problem
        "str": String.self,
        "int": Int.self,
        "foo": Foo.self
    ]

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

    required init(from decoder: Decoder) throws {

        // Get the container for the CodingKeys
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // Get the key to look up the concrete type
        typeKey = try container.decode(String.self, forKey:.typeKey)

        // Attempt to get the concrete type from the key
        guard let concreteType = AnyDecodableWrapper.decodableTypesLookup[typeKey] else {
            value = nil
            return
        }

        // Attempt to decode an instance of the concrete type
        let concreteObject = try container.decode(concreteType, forKey: .value)

        value = concreteObject
    }

    let typeKey : String
    let value   : Any?
}

Проблема заключается в том, что строка, присваивающая темп concreteObject, жалуется на следующее ...

Неоднозначная ссылка на член 'decode (_: forKey:)'

Это, конечно, потому что тип, возвращаемый из словаря, это Decodable.Type, а не что-то вроде 'String.self', таким образом, это не уверен, какую decode перегрузку использовать.

Итак, если у вас есть конкретный тип, хранящийся в переменной Any.Type, как вы можете передать это корректной перегрузке декодирования?

Ответы [ 2 ]

1 голос
/ 24 января 2020

KeyedDecodingContainer.decode(_:forKey:) (и методы decode(...) других контейнеров) - это обобщенный метод c, принимающий параметр типа как обобщенный c вход. Для вызова метода Swift необходимо знать тип generi c статически во время выполнения. Хотя у вас есть значение Decodable.Type, для отправки вызова метода Swift потребуется тип speci c во время компиляции.

Как упоминалось в комментарии, самое простое изменение, которое вы можете сделать в приведенном выше коде, - это снизить обобщение до заданных c decode<T>(_:forKey:) вызовов:

switch typeKey {
case "int": value = try container.decode(Int.self,    forKey: .value)
case "str": value = try container.decode(String.self, forKey: .value)
case "foo": value = try container.decode(Foo.self,    forKey: .value)
default:    value = nil
}

Это указывает компилятору тип generi c для отправки вызова , В зависимости от ваших конкретных потребностей c, могут быть и другие решения, но это суть того, что вам в конечном итоге нужно сделать. (Вы действительно можете обернуть вызовы в замыкания, чтобы добавить слой косвенности.)


Это предполагает, что у вас есть для соответствия JSON, который указывает тип inline (например, { "type": "int", "value": 42 }), но если у вас есть контроль над данными JSON, вы, вероятно, можете избежать этого, создав, скажем, enum, который представляет все возможные типы, которые вы ожидаете (как изложено в Как иметь дело с полностью динамически c JSON ответы ):

enum IntOrStringOrFoo: Decodable {
    case int(Int)
    case string(String)
    case foo(Foo)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = .int(try container.decode(Int.self))
        } catch DecodingError.typeMismatch {
            do {
                self = .string(try container.decode(String.self))
            } catch DecodingError.typeMismatch {
                self = .foo(try container.decode(Foo.self))
            }
        }
    }
}

Вместо поиска селектора типа в декодированных данных, вы можете попытаться декодировать как типы, которые вы нужно, и придерживаться того, что удается. Это предполагает, что типы не перекрываются (например, могут быть декодируемыми как один другой, например Int8 и Int, в этом случае вам нужно быть очень осторожным с порядком, в котором вы декодируете).

0 голосов
/ 24 января 2020

Вы всегда можете использовать гигантский оператор switch, проверяющий каждое возможное значение (ew), но если вы собираетесь это сделать, гораздо лучшим способом будет использование стирания типа. Недостатком является то, что вы в значительной степени должны пройти через go все ваши возможные классы и объявляйте их как соответствующие или реализующие забавный c, который возвращает декодируемый тип - который может быть громоздким.

В идеале мы могли бы просто получить «готовую» поддержку для всего, что Decodable.

Для этого нам потребуется языковая функция, называемая открытием экзистенциалов - есть отличная и подробная запись. проблем, с которыми вы сталкиваетесь в этом вопросе ( Протокол не соответствует самому себе? )

В любом случае, понятия не имею, работает ли это, но я был в состоянии получить что-то для компиляции этого кажется, что делает то, что вы хотите:

class AnyDecodableWrapper : Decodable {

    static let decodableTypesLookup: [String: Decodable] = [
        "str": "",
        "int": 0
    ]

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

    private struct DecodableObject: Decodable {}

    required init(from decoder: Decoder) throws {

        // Get the container for the CodingKeys
        let container = try decoder.container(keyedBy: CodingKeys.self)

        typeKey = try container.decode(String.self, forKey:.typeKey)

        guard let concreteType = AnyDecodableWrapper.decodableTypesLookup[typeKey] else {
            value = nil
            return
        }

        let concreteObject: DecodableObject = try container.unambiguousDecode(concreteType, forKey: .value)
        value = concreteObject
    }

    let typeKey : String
    let value   : Any?
}

extension KeyedDecodingContainer {
    func unambiguousDecode<T: Decodable>(_ decodableType: Decodable, forKey key: KeyedDecodingContainer<K>.Key) throws -> T {
        let decodable = type(of: decodableType) as! T.Type
        return try decode(decodable, forKey: key)
    }
}

Объяснение:

Проблема в том, что компилятор не может определить, какой из 16 decode: методов использовать в KeyedDecodingContainer.

Поскольку у swift нет открытых экзистенциалов, он не может сказать, что передача аргумента Decodable.Type такая же, как T.Type where T: Decodable

Итак, первое, что я сделал, - это работа с Decodable напрямую вместо Decodable.Type. Это означало, что нужно изменить decodableTypesLookup на [String: Decodable] и создать собственный метод декодирования для KeyedDecodingContainer, в котором вместо мета-типа будет использоваться Decodable (как в стандартном decode: fun c take).

Здесь меня беспокоит следующая строка:

let decodable = type(of: decodableType) as! T.Type

Если это не так, возможно, есть способ привести к обобщенному типу c вместо использования as!?

Так или иначе, из-за этого у меня произошла ошибка (Generi c параметр T не может быть выведен) на

let concreteObject = try container.unambiguousDecode(concreteType, forKey: .value)

, и это только потому, что вы не можете получить конкретный бетон Decodable() и он понятия не имеет, каким должен быть конкретный объект.

Чтобы обойти это, я просто создал одноразовый выброс:

private struct DecodableObject: Decodable {}

Просто чтобы я мог сказать компилятору, что будь конкретным объектом:

let concreteObject: DecodableObject = try container.unambiguousDecode(concreteType, forKey: .value)

тогда мы можем явно скрыть это ожидаемым типом Any? (возможно, AnyObject будет лучше tbh)

Надеюсь, что это работает ..

...