解码器是否需要具有密钥的外部数据表示形式?

Does Decoder require an external data representation that has keys?

提问人:3366784 提问时间:9/14/2023 更新时间:9/14/2023 访问量:63

问:

我做了什么?

我阅读了 Apple 关于该主题的文档

Swift 标准库定义了一种标准化的数据编码和解码方法。

我意识到我遇到的所有示例都从具有 、 和 (custom) 等键的外部数据表示中解码数据。JSONPLISTXML

我遇到过一个外部数据表示没有键的用例。 例如,从蓝牙设备读取数据时。您会收到格式为 .ArrayUInt8

在探索 Apple 的文档和自定义实现时,我意识到所有外部数据表示都有键。Decoder

我想知道 Apple 的解决方案是否留有足够的回旋余地来处理不使用键的外部数据表示,即 .ArrayUInt8

快速 解码

评论

0赞 Sweeper 9/14/2023
有 和 .这就是你要找的吗?此外,a 不是外部表示。你是说任意二进制数据吗?如果可以输出任意字节,则可以按照所需的任何方式对密钥进行编码。UnkeyedDecodingContainerUnkeyedEncodingContainerArray<UInt8>
0赞 3366784 9/14/2023
@Sweeper 对不起,是的,这是根据蓝牙标准格式化的方式。然后对数组进行处理,以形成具有各种类型(整数、浮点数、布尔值)属性的数据模型。对不起,如果我错过了对这里的理解。我知道.Array<UInt8>DataUnkeyedDecodingContainer
0赞 Sweeper 9/14/2023
好吧,如果你知道,有什么好问的呢?它旨在处理可编码事物的数组,例如“具有各种类型(整数、浮点数、布尔值)属性的数据模型”。UnkeyedDecodingContainer
0赞 Sweeper 9/14/2023
事实上,它是一个并不重要。JSON 也只是一个 .实际上,计算机中的一切都只是.我不熟悉蓝牙的协议,但如果你说“具有各种类型属性(整数、浮点数、布尔值)的数据模型”,听起来数据确实有识别每个属性的键。在任何情况下,如果某些内容不受支持,解码器/编码器可能会抛出错误。Array<UInt8>Array<UInt8>Array<UInt8>
1赞 Itai Ferber 9/14/2023
如果您期望解码的唯一类型是您控制的类型(即,它们知道只请求未加密的容器),那么完全避免通过并编写自己的解码原语可能会更简单,更容易,除非您期望这些类型也被编码/解码为其他格式。Decodable

答:

2赞 Itai Ferber 9/14/2023 #1

Decoder.container(keyedBy:) 被记录为在没有可供解码的键控数据时抛出错误 ()。在这种程度上,如果 a 表示一种根本无法支持键控数据的格式,那么当请求键控数据时,它总是会抛出错误。DecodingError.typeMismatchDecoderDecoder

但是,这样的类型对于任意类型并不是很有用,因为大多数类型默认从键控数据进行编码和解码;这是使用编译器的综合/一致性时的默认行为。DecoderEncodableDecodable

如果你使用这样的方法只解码你控制的类型(并且只知道请求非密钥容器),那么完全避免使用API,并编写自己的编码和解码工具可能会更简单、更容易。DecoderCodable

根据您对格式的描述,听起来这种格式不太可能适合 ,反之亦然。(这也没关系! 并非旨在完美适合所有数据格式。CodableCodable

评论

0赞 3366784 9/14/2023
这回答了我的问题,非常感谢。我很想知道 Apple 在解码时使用什么来初始化或设置每个属性。我认为我还没有理解创建自己的解码原语,即使数据是键控的。
1赞 Itai Ferber 9/14/2023
@3366784回复:“解码原语”——对不起,我的措辞很糟糕。我已经更新了答案,说“解码工具”,就像你自己的解析代码一样。Sweeper 的回答也更好地涵盖了这一点。
1赞 Itai Ferber 9/14/2023
至于更多地了解如何实现,例如,。 作品:代码是开源的,可查看的!这是 JSONDecoder 的当前实现,以及更新包中的重写JSONDecoderswift-foundation
1赞 Itai Ferber 9/14/2023
当前实现的要点:用于解析和解码所有 JSON,然后提供对数据的“视图”,根据需要将其转换为用户请求的类型。在即将到来的重写中,JSON 的解码是在请求值时延迟完成的,但想法大致相同。JSONSerializationJSONDecoder
1赞 Itai Ferber 9/15/2023
以下是其工作原理的更详细概述:forums.swift.org/t/...
2赞 Sweeper 9/14/2023 #2

我想你可能误解了可编码 API 的用途。它不是用来编写解析和写入二进制数据的逻辑 - 你仍然需要自己编写。

Codable允许您通过键控、非键控和单值容器以更抽象的方式进行编码和解码。你可以这样说:

  • 获取具有这些密钥的密钥容器
  • 获取无密钥容器
  • 使用 YYY 密钥对 XXX 进行编码
  • 将 XXX 键的值解码为 YYY 类型
  • 将 XXX 编码为此无密钥容器的下一个值
  • 将此无密钥容器的下一个值解码为 XXX 类型

你写你希望你的模型如何根据这些操作进行编码/解码,然后 / 将对底层二进制数据做相应的事情。DecoderEncoder

例如,如果要将具有 5 个整数的 JSON 数组解码为具有 5 个属性的结构体,则可以编写:Int

struct MyModel: Decodable {
    let a, b, c, d, e: Int
    
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        a = try container.decode(Int.self)
        b = try container.decode(Int.self)
        c = try container.decode(Int.self)
        d = try container.decode(Int.self)
        e = try container.decode(Int.self)
    }
}

对于 JSON,表示“准备解码 JSON 数组”,而“decode(Int.self)”表示“将下一个数组元素读取为 ”。请注意,您不必编写如何准确解析 JSON。unkeyedContainer()Int

Decoder/Encoder实现不必支持所有这些操作。如果实现出于任何原因不支持某些方法,则在调用该方法时可能会引发错误。

对于一系列字节,您可以实现自己的仅支持,并且会根据 读取 x 个字节,并将其转换为正确的类型,有点像 BinaryReaderDecoderunkeyedContainer()decode(T.self)T

// just rough sketch of an implementation, not complete
struct _BinaryDecoder: Decoder, UnkeyedDecodingContainer {
    var isAtEnd: Bool {
        data.count >= currentIndex
    }
    
    var currentIndex: Int = 0
    
    func hasNBytes(_ n: Int) -> Bool {
        data.count - currentIndex >= n
    }
    
    mutating func decode(_ type: Float.Type) throws -> Float {
        guard hasNBytes(4) else { throw someError }
        let f = data.withUnsafeBytes { buffer in
            buffer.loadUnaligned(fromByteOffset: currentIndex, as: Float.self)
        }
        currentIndex += 4
        return f
    }
    
    mutating func decode(_ type: Int32.Type) throws -> Int32 {
        guard hasNBytes(4) else { throw someError }
        let i = data.withUnsafeBytes { buffer in
            buffer.loadUnaligned(fromByteOffset: currentIndex, as: Int32.self)
        }
        currentIndex += 4
        return i
    }
    
    let data: Data
    
    func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        self
    }
}

struct BinaryDecoder {
    func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
        try T.init(from: _BinaryDecoder(data: data))
    }
}

然后,您可以在模型中实现,如下所示:init(from:)

var container = try decoder.unkeyedContainer()
let flags = try container.decode(UInt16.self)
switch flags {
    // use container.decode to initialise different properties based on flags
    // e.g. if the next bytes represent two 32-bit signed ints, and then a float
    case 0:
        property1 = try container.decode(Int32.self)
        property2 = try container.decode(Int32.self)
        property3 = try container.decode(Float.self)
}

但是,如果您只打算解码这一种类型的二进制数据,那么在不使用 .Codable

评论

0赞 3366784 9/15/2023
这是一个很好的起点,我很欣赏这个例子。已将此标记为已接受的正确答案。
0赞 3366784 9/15/2023
我没有意识到某些逻辑可能应该存在于给定模型的 init(from:) 方法中,因为每个模型都具有独特的动态逻辑,给定标志和模型属性(顺序和类型)需要遵循这些逻辑。