【问题标题】:round trip Swift number types to/from Data往返数据的往返 Swift 数字类型
【发布时间】:2016-10-27 16:33:02
【问题描述】:

由于 Swift 3 倾向于 Data 而不是 [UInt8],我试图找出最有效/惯用的编码/解码 swift 的各种数字类型(UInt8、Double、Float、Int64 等)作为数据对象。

this answer for using [UInt8],不过好像在用各种我在Data上找不到的指针API。

我基本上想要一些看起来像这样的自定义扩展:

let input = 42.13 // implicit Double
let bytes = input.data
let roundtrip = bytes.to(Double) // --> 42.13

真正让我难以理解的部分是,我浏览了一堆文档,是如何从任何基本结构(所有数字都是)。在 C 语言中,我会在它前面加上一个 & 符号,然后就可以了。

【问题讨论】:

标签: swift swift3 swift-data


【解决方案1】:

注意: 代码现已更新为 Swift 5 (Xcode 10.2)。 (可以在编辑历史中找到 Swift 3 和 Swift 4.2 版本。)现在还可能正确处理未对齐的数据。

如何从一个值创建Data

从 Swift 4.2 开始,可以简单地从值创建数据

let value = 42.13
let data = withUnsafeBytes(of: value) { Data($0) }

print(data as NSData) // <713d0ad7 a3104540>

解释:

  • withUnsafeBytes(of: value) 使用覆盖值的原始字节的缓冲区指针调用闭包。
  • 原始缓冲区指针是一个字节序列,因此Data($0) 可用于创建数据。

如何从Data 中检索值

从 Swift 5 开始,DatawithUnsafeBytes(_:) 调用闭包,并带有“无类型”UnsafeMutableRawBufferPointer 到字节。 load(fromByteOffset:as:) 方法从内存中读取值:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes {
    $0.load(as: Double.self)
}
print(value) // 42.13

这种方法有一个问题:它要求内存为类型的属性对齐(这里:对齐到 8 字节地址)。但这不能保证,例如如果数据是作为另一个 Data 值的切片获得的。

因此将字节复制到值更安全:

let data = Data([0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
var value = 0.0
let bytesCopied = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
assert(bytesCopied == MemoryLayout.size(ofValue: value))
print(value) // 42.13

解释:

copyBytes() 的返回值是复制的字节数。它等于目标缓冲区的大小,如果数据不包含足够的字节,则小于。

通用解决方案#1

上面的转换现在可以很容易地实现为struct Data的泛型方法:

extension Data {

    init<T>(from value: T) {
        self = Swift.withUnsafeBytes(of: value) { Data($0) }
    }

    func to<T>(type: T.Type) -> T? where T: ExpressibleByIntegerLiteral {
        var value: T = 0
        guard count >= MemoryLayout.size(ofValue: value) else { return nil }
        _ = Swift.withUnsafeMutableBytes(of: &value, { copyBytes(to: $0)} )
        return value
    }
}

此处添加了约束T: ExpressibleByIntegerLiteral,以便我们可以轻松地将值初始化为“零”——这并不是真正的限制,因为此方法无论如何都可以用于“trival”(整数和浮点)类型,请参阅下面。

例子:

let value = 42.13 // implicit Double
let data = Data(from: value)
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = data.to(type: Double.self) {
    print(roundtrip) // 42.13
} else {
    print("not enough data")
}

同样,您可以将 数组 转换为 Data 并返回:

extension Data {

    init<T>(fromArray values: [T]) {
        self = values.withUnsafeBytes { Data($0) }
    }

    func toArray<T>(type: T.Type) -> [T] where T: ExpressibleByIntegerLiteral {
        var array = Array<T>(repeating: 0, count: self.count/MemoryLayout<T>.stride)
        _ = array.withUnsafeMutableBytes { copyBytes(to: $0) }
        return array
    }
}

例子:

let value: [Int16] = [1, Int16.max, Int16.min]
let data = Data(fromArray: value)
print(data as NSData) // <0100ff7f 0080>

let roundtrip = data.toArray(type: Int16.self)
print(roundtrip) // [1, 32767, -32768]

通用解决方案#2

上述方法有一个缺点:它实际上只适用于“琐碎” 整数和浮点类型等类型。 “复杂”类型,例如 ArrayString 有(隐藏的)指向底层存储的指针,不能 通过复制结构本身来传递。它也不适用于 引用类型,它们只是指向真实对象存储的指针。

所以解决这个问题,可以

  • 定义一个协议,定义转换为Data 并返回的方法:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
    
  • 将转换实现为协议扩展中的默认方法:

    extension DataConvertible where Self: ExpressibleByIntegerLiteral{
    
        init?(data: Data) {
            var value: Self = 0
            guard data.count == MemoryLayout.size(ofValue: value) else { return nil }
            _ = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
            self = value
        }
    
        var data: Data {
            return withUnsafeBytes(of: self) { Data($0) }
        }
    }
    

    我在这里选择了一个 failable 初始化程序,它检查提供的字节数 匹配类型的大小。

  • 最后声明符合所有可以安全地转换为 Data 并返回的类型:

    extension Int : DataConvertible { }
    extension Float : DataConvertible { }
    extension Double : DataConvertible { }
    // add more types here ...
    

这使得转换更加优雅:

let value = 42.13
let data = value.data
print(data as NSData) // <713d0ad7 a3104540>

if let roundtrip = Double(data: data) {
    print(roundtrip) // 42.13
}

第二种方法的优点是您不会无意中进行不安全的转换。缺点是您必须明确列出所有“安全”类型。

您还可以为需要非平凡转换的其他类型实现协议,例如:

extension String: DataConvertible {
    init?(data: Data) {
        self.init(data: data, encoding: .utf8)
    }
    var data: Data {
        // Note: a conversion to UTF-8 cannot fail.
        return Data(self.utf8)
    }
}

或者在你自己的类型中实现转换方法来做任何事情 必要的,所以序列化和反序列化一个值。

字节顺序

以上方法都没有进行字节序转换,数据总是在 主机字节顺序。对于独立于平台的表示(例如 “big endian”又名“network”字节顺序),使用相应的整数 属性分别初始化器。例如:

let value = 1000
let data = value.bigEndian.data
print(data as NSData) // <00000000 000003e8>

if let roundtrip = Int(data: data) {
    print(Int(bigEndian: roundtrip)) // 1000
}

当然这个转换一般也可以,在泛型中 转换方法。

【讨论】:

  • 我们必须制作初始值的var 副本是否意味着我们要复制字节两次?在我当前的用例中,我将它们转换为数据结构,因此我可以append 将它们转换为不断增长的字节流。在直接 C 中,这就像*(cPointer + offset) = originalValue 一样简单。所以字节只复制一次。
  • @TravisGriggs:复制 int 或 float 很可能无关紧要,但您可以在 Swift 中做类似的事情。如果你有一个ptr: UnsafeMutablePointer&lt;UInt8&gt;,那么你可以通过类似于UnsafeMutablePointer&lt;T&gt;(ptr + offset).pointee = value 的东西分配给引用的内存,这与你的Swift 代码非常对应。有一个潜在的问题:一些处理器只允许 aligned 内存访问,例如您不能将 Int 存储在奇数内存位置。我不知道这是否适用于当前使用的 Intel 和 ARM 处理器。
  • @TravisGriggs: (cont'd) ... 这也要求已经创建了足够大的 Data 对象,而在 Swift 中你只能创建 并初始化数据对象,因此您可能在初始化期间有一个额外的零字节副本。 – 如果您需要更多详细信息,那么我建议您发布一个新问题。
  • @HansBrende:恐怕目前还不可能。它需要extension Array: DataConvertible where Element: DataConvertible。这在 Swift 3 中是不可能的,但计划在 Swift 4 中实现(据我所知)。比较github.com/apple/swift/blob/master/docs/…中的“条件一致性”
  • @m_katsifarakis: 会不会是你把Int.self 打错了Int.Type
【解决方案2】:

就我而言,Martin R 的回答有所帮助,但结果却相反。所以我对他的代码做了一点小改动:

extension UInt16 : DataConvertible {

    init?(data: Data) {
        guard data.count == MemoryLayout<UInt16>.size else { 
          return nil 
        }
    self = data.withUnsafeBytes { $0.pointee }
    }

    var data: Data {
         var value = CFSwapInt16HostToBig(self)//Acho que o padrao do IOS 'e LittleEndian, pois os bytes estavao ao contrario
         return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }
}

问题与 LittleEndian 和 BigEndian 有关。

【讨论】:

    【解决方案3】:

    您可以使用withUnsafePointer 获得指向可变对象的不安全指针:

    withUnsafePointer(&input) { /* $0 is your pointer */ }
    

    我不知道如何为不可变对象获取一个,因为 inout 运算符仅适用于可变对象。

    这在您链接到的答案中得到了证明。

    【讨论】:

      猜你喜欢
      • 2011-02-26
      • 2015-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-12
      • 1970-01-01
      相关资源
      最近更新 更多