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 Data
partir 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 Data
invoca o encerramento com um "não digitado" UnsafeMutableRawBufferPointer
para 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 Data
valor.
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 Data
está 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 Data
e 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 String
tê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 Data
e 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 Data
e 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.