题 往返数据的往返Swift数字类型


随着Swift 3倾向于 Data 代替 [UInt8],我试图找出最有效/惯用的编码/解码方式,将各种数字类型(UInt8,Double,Float,Int64等)作为数据对象。

这个答案使用[UInt8],但它似乎使用我在Data上找不到的各种指针API。

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

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

真正逃避我的部分,我已经查看了一堆文档,我是如何从任何基本结构(所有数字都是)获得某种指针的东西(OpaquePointer或BufferPointer或UnsafePointer?)。在C中,我只会在它前面拍一个&符号,然后就可以了。


62
2018-06-25 00:31


起源


stackoverflow.com/a/43244973/2303865 - Leo Dabus


答案:


如何创造 Data 从一个价值

struct Data 有一个初始化器

public init(bytes: UnsafeRawPointer, count: Int)

可以用与问题的各种答案类似的方式使用 如何在swift中将double转换为字节数组? 你链接到:

let input = 42.13
var value = input
let data = withUnsafePointer(to: &value) {
    Data(bytes: UnsafePointer($0), count: MemoryLayout.size(ofValue: input))
}
print(data as NSData) // <713d0ad7 a3104540>

正如@zneak所说,你可以只取一个地址 变量, 因此,变量副本是用 var value = value。 在早期的Swift版本中,您可以通过制作 函数参数本身是一个变量,不再支持。

但是,使用初始化程序更容易

public init<SourceType>(buffer: UnsafeBufferPointer<SourceType>)

代替:

let input = 42.13
var value = input
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
print(data as NSData) // <713d0ad7 a3104540>

请注意通用占位符 SourceType 从...自动推断 上下文。

如何从中检索值 Data

NSData 有一个 bytes 用于访问底层存储的属性。 struct Data 有一个通用的

public func withUnsafeBytes<ResultType, ContentType>(_ body: @noescape (UnsafePointer<ContentType>) throws -> ResultType) rethrows -> ResultType

相反,可以像这样使用:

let data = Data(bytes: [0x71, 0x3d, 0x0a, 0xd7, 0xa3, 0x10, 0x45, 0x40])
let value = data.withUnsafeBytes { (ptr: UnsafePointer<Double>) -> Double in
    return ptr.pointee
}
print(value) // 42.13

如果 ContentType 可以从上下文推断出它不需要指定 在封闭中,所以这可以简化为

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

通用解决方案#1

现在可以轻松地将上述转换实现为通用方法 struct Data

extension Data {

    init<T>(from value: T) {
        var value = value
        self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T {
        return self.withUnsafeBytes { $0.pointee }
    }
}

例:

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

let roundtrip = data.to(type: Double.self)
print(roundtrip) // 42.13

同样,你可以转换 阵列 至 Data 然后回来:

extension Data {

    init<T>(fromArray values: [T]) {
        var values = values
        self.init(buffer: UnsafeBufferPointer(start: &values, count: values.count))
    }

    func toArray<T>(type: T.Type) -> [T] {
        return self.withUnsafeBytes {
            [T](UnsafeBufferPointer(start: $0, count: self.count/MemoryLayout<T>.stride))
        }
    }
}

例:

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

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

通用解决方案#2

上述方法有一个缺点:如 如何在swift中将double转换为字节数组?, 它实际上只适用于“简单” 类型如整数和浮点类型。 “复杂”类型喜欢 ArrayString 有(隐藏)指向底层存储的指针,但不能 通过复制结构本身来传递。它也不适用 引用类型,它们只是指向真实对象存储的指针。

可以解决这个问题

  • 定义一个定义转换方法的协议 Data 然后回来:

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

    extension DataConvertible {
    
        init?(data: Data) {
            guard data.count == MemoryLayout<Self>.size else { return nil }
            self = data.withUnsafeBytes { $0.pointee }
        }
    
        var data: Data {
            var value = self
            return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
        }
    }
    

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

  • 最后声明对可以安全转换为的所有类型的一致性 Data 然后回来:

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

这使得转换更加优雅:

let input = 42.13
let data = input.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 self.data(using: .utf8)!
    }
}

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


150
2018-06-25 01:12



很棒的解释。希望我可以给两个上升票。 - Travis Griggs
@TravisGriggs:复制int或float很可能不相关,但是你 能够 在Swift做类似的事情。如果你有 ptr: UnsafeMutablePointer<UInt8> 那么你可以通过类似的东西分配给引用的内存 UnsafeMutablePointer<T>(ptr + offset).pointee = value 它与您的Swift代码密切对应。有一个潜在的问题:某些处理器只允许 对齐 内存访问,例如您不能将Int存储在奇数存储器位置。我不知道这是否适用于目前使用的Intel和ARM处理器。 - Martin R
@TravisGriggs :(续)...这也要求已经创建了一个足够大的Data对象,而在Swift中你只能创建 并初始化 Data对象,因此在初始化期间可能有一个零字节的附加副本。 - 如果您需要更多详细信息,我建议您发布一个新问题。 - Martin R
@HansBrende:恐怕目前不可能。这需要一个 extension Array: DataConvertible where Element: DataConvertible。这在Swift 3中是不可能的,但是计划用于Swift 4(据我所知)。比较中的“条件一致性” github.com/apple/swift/blob/master/docs/... - Martin R
我不知道! - vadian


你可以得到一个不安全的指针 易变的 使用对象 withUnsafePointer

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

我不知道为不可变对象获取一个的方法,因为inout运算符只适用于可变对象。

这与你所链接的答案有关。


3
2018-06-25 00:55





就我而言, 马丁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有关。


1
2017-12-09 19:11