往返转换 Swift 数字类型与数据之间

114

随着 Swift 3 偏向于使用 Data 而不是 [UInt8],我正在尝试找出将 Swift 的各种数字类型(UInt8、Double、Float、Int64 等)编码/解码为 Data 对象的最有效/惯用方式。

有一个关于使用 [UInt8]这个答案,但它似乎在使用一些我在 Data 上找不到的指针 API。

我想要创建一些自定义扩展,大致如下:

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

真正让我困惑的部分是,我已经查阅了很多文档,但我不知道如何从任何基本结构体(所有数字都是基本结构体)中获取某种指针(OpaquePointer、BufferPointer或UnsafePointer?)。在C语言中,我只需要在它前面加上一个&符号,就可以搞定了。

https://dev59.com/R1gQ5IYBdhLWcg3wJgiO#43244973 - Leo Dabus
3个回答

294

注意: 代码已经更新到 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

解释:

  • withUnsafeMutableBytes(of:_:) 调用闭包,该闭包使用可变缓冲区指针覆盖值的原始字节。
  • DataProtocolcopyBytes(to:) 方法(Data符合该协议)将字节从数据复制到该缓冲区。

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,以便我们可以轻松地将值初始化为“零” - 这实际上并不是一种限制,因为该方法仍可用于“平凡”的(整数和浮点数)类型,详见下文。
示例:
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 这样的“复杂”类型具有(隐藏的)指向底层存储的指针,不能仅通过复制结构体本身来传递。它也无法处理只是指向真实对象存储的指针的引用类型。

为了解决这个问题,可以:

  • Define a protocol which defines the methods for converting to Data and back:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
    
  • Implement the conversions as default methods in a protocol extension:

    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) }
        }
    }
    

    I have chosen a failable initializer here which checks that the number of bytes provided matches the size of the type.

  • And finally declare conformance to all types which can safely be converted to Data and back:

    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)
    }
}

或者在您自己的类型中实现转换方法,以执行必要的操作来序列化和反序列化值。

字节顺序

上述方法中不进行任何字节顺序转换,数据始终处于主机字节顺序。为了获得平台无关的表示(例如“大端”或“网络”字节顺序),请使用相应的整数属性或初始化程序。例如:

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
}

当然,这种转换也可以在通用的转换方法中完成。

1
@TravisGriggs:复制int或float可能不相关,但在Swift中可以做类似的事情。如果您有一个ptr: UnsafeMutablePointer<UInt8>,那么您可以通过类似于UnsafeMutablePointer<T>(ptr + offset).pointee = value的方式分配给引用的内存,这与您的Swift代码非常相似。有一个潜在的问题:某些处理器仅允许对齐内存访问,例如,您不能将Int存储在奇数内存位置。我不知道是否适用于当前使用的Intel和ARM处理器。 - Martin R
1
@TravisGriggs: (续) ...此外,这要求已经创建了足够大的Data对象,在Swift中,您只能创建并初始化 Data对象,因此在初始化期间可能会有额外的零字节副本。-如果您需要更多详细信息,则建议您发布一个新问题。 - Martin R
@TravisGriggs: Int32(data: data.subdata(in: 0 ..< 3)) 应该可以工作,但我不知道是否涉及另一个副本。也可以通过添加另一个参数来扩展上述方法,例如 Int32(data: data, atOffset: 4) - Martin R
2
@HansBrende:恐怕目前还不可能。这需要一个 extension Array: DataConvertible where Element: DataConvertible。在Swift 3中是不可能的,但据我所知,计划在Swift 4中实现。请参阅https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#conditional-conformances中的“条件一致性”。 - Martin R
1
@m_katsifarakis:你是否将 Int.self 错误地输入为 Int.Type - Martin R
显示剩余29条评论

3

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

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

我不知道如何对不可变对象执行inout操作,因为inout操作符只适用于可变对象。

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


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有关。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接