Tipos de números Swift de ida e volta para / de dados

94

Com o Swift 3 inclinado em Datavez de [UInt8], estou tentando descobrir qual é a maneira mais eficiente / idiomática de codificar / decodificar swifts em vários tipos de números (UInt8, Double, Float, Int64, etc) como objetos de dados.

Existe esta resposta para usar [UInt8] , mas parece estar usando várias APIs de ponteiro que não consigo encontrar em Data.

Eu gostaria de basicamente algumas extensões personalizadas que se parecem com:

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

A parte que realmente me escapa, eu olhei em vários documentos, é como posso obter algum tipo de ponteiro (OpaquePointer ou BufferPointer ou UnsafePointer?) De qualquer estrutura básica (que são todos os números). Em C, eu apenas colocaria um "e" comercial na frente dele, e pronto.

Travis Griggs
fonte

Respostas:

256

Nota: O código foi atualizado para Swift 5 (Xcode 10.2) agora. (As versões Swift 3 e Swift 4.2 podem ser encontradas no histórico de edição.) Além disso, dados possivelmente não alinhados agora são tratados corretamente.

Como criar a Datapartir de um valor

A partir do Swift 4.2, os dados podem ser criados a partir de um valor simplesmente com

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

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

Explicação:

  • withUnsafeBytes(of: value) invoca o encerramento com um ponteiro de buffer cobrindo os bytes brutos do valor.
  • Um ponteiro de buffer bruto é uma sequência de bytes, portanto, Data($0)pode ser usado para criar os dados.

Como recuperar um valor de Data

A partir do Swift 5, o withUnsafeBytes(_:)de Datainvoca o encerramento com um "não digitado" UnsafeMutableRawBufferPointerpara os bytes. O load(fromByteOffset:as:)método que lê o valor da memória:

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

Há um problema com essa abordagem: ela requer que a memória seja alinhada por propriedades ao tipo (aqui: alinhada a um endereço de 8 bytes). Mas isso não é garantido, por exemplo, se os dados foram obtidos como uma fatia de outro Datavalor.

Portanto, é mais seguro copiar os bytes para o valor:

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

Explicação:

  • withUnsafeMutableBytes(of:_:) invoca o encerramento com um ponteiro de buffer mutável cobrindo os bytes brutos do valor.
  • O copyBytes(to:)método de DataProtocol(com o qual Dataestá em conformidade) copia bytes dos dados para esse buffer.

O valor de retorno de copyBytes()é o número de bytes copiados. É igual ao tamanho do buffer de destino ou menos se os dados não contiverem bytes suficientes.

Solução genérica # 1

As conversões acima agora podem ser facilmente implementadas como métodos genéricos de 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
    }
}

A restrição T: ExpressibleByIntegerLiteralé adicionada aqui para que possamos inicializar facilmente o valor como “zero” - isso não é realmente uma restrição porque este método pode ser usado com tipos “trival” (inteiro e ponto flutuante) de qualquer maneira, veja abaixo.

Exemplo:

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

Da mesma forma, você pode converter matrizes para Datae de volta:

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

Exemplo:

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]

Solução genérica # 2

A abordagem acima tem uma desvantagem: na verdade, funciona apenas com tipos "triviais", como inteiros e tipos de ponto flutuante. Os tipos "complexos" gostam Array e Stringtêm ponteiros (ocultos) para o armazenamento subjacente e não podem ser transmitidos apenas copiando a própria estrutura. Também não funcionaria com tipos de referência que são apenas ponteiros para o armazenamento de objeto real.

Então resolva esse problema, pode-se

  • Defina um protocolo que define os métodos de conversão para Datae de volta:

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
  • Implemente as conversões como métodos padrão em uma extensão de protocolo:

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

    Escolhi um inicializador failable aqui, que verifica se o número de bytes fornecidos corresponde ao tamanho do tipo.

  • E, finalmente, declara conformidade com todos os tipos que podem ser convertidos com segurança para Datae de volta:

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

Isso torna a conversão ainda mais elegante:

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

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

A vantagem da segunda abordagem é que você não pode fazer conversões não seguras inadvertidamente. A desvantagem é que você deve listar todos os tipos "seguros" explicitamente.

Você também pode implementar o protocolo para outros tipos que exigem uma conversão não trivial, como:

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

ou implemente os métodos de conversão em seus próprios tipos para fazer o que for necessário para serializar e desserializar um valor.

Ordem de bytes

Nenhuma conversão de ordem de byte é feita nos métodos acima, os dados estão sempre na ordem de byte do host. Para uma representação independente de plataforma (por exemplo, “big endian” ou “rede” ordem de bytes), use as propriedades de inteiros correspondentes resp. inicializadores. Por exemplo:

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
}

É claro que essa conversão também pode ser feita de maneira geral, no método de conversão genérico.

Martin R
fonte
O fato de termos que fazer uma varcópia do valor inicial significa que estamos copiando os bytes duas vezes? No meu caso de uso atual, estou transformando-os em estruturas de dados, para que possa transformá-los em appendum fluxo crescente de bytes. Em C direto, isso é tão fácil quanto *(cPointer + offset) = originalValue. Portanto, os bytes são copiados apenas uma vez.
Travis Griggs
1
@TravisGriggs: Copiar um int ou float provavelmente não será relevante, mas você pode fazer coisas semelhantes no Swift. Se você tiver um ptr: UnsafeMutablePointer<UInt8>, poderá atribuir à memória referenciada por meio de algo UnsafeMutablePointer<T>(ptr + offset).pointee = valueque corresponda de perto ao seu código Swift. Existe um problema potencial: alguns processadores permitem apenas acesso alinhado à memória, por exemplo, você não pode armazenar um Int em um local de memória estranho. Não sei se isso se aplica aos processadores Intel e ARM usados ​​atualmente.
Martin R
1
@TravisGriggs: (continuação) ... Isso também requer que um objeto de dados suficientemente grande já tenha sido criado, e em Swift você só pode criar e inicializar o objeto de dados, então você pode ter uma cópia adicional de zero bytes durante o inicialização. - Se precisar de mais detalhes, sugiro que poste uma nova pergunta.
Martin R
2
@HansBrende: Receio que atualmente não seja possível. Isso exigiria um extension Array: DataConvertible where Element: DataConvertible. Isso não é possível no Swift 3, mas planejado para o Swift 4 (até onde eu sei). Compare as "condicionalidades" em github.com/apple/swift/blob/master/docs/…
Martin R
1
@m_katsifarakis: Poderia ser que você digitado incorretamente Int.selfcomo Int.Type?
Martin R
3

Você pode obter um ponteiro não seguro para objetos mutáveis usando withUnsafePointer:

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

Não sei como obter um para objetos imutáveis, porque o operador inout só funciona com objetos mutáveis.

Isso é demonstrado na resposta que você vinculou.

zneak
fonte
2

No meu caso, a resposta de Martin R ajudou, mas o resultado foi invertido. Então, fiz uma pequena mudança em seu código:

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

O problema está relacionado com LittleEndian e BigEndian.

Beto Caldas
fonte