Verilere gidiş / dönüş Swift sayı türleri


97

Bunun Datayerine Swift 3'e eğilerek [UInt8], çeşitli sayı türlerini (UInt8, Double, Float, Int64, vb.) Veri nesneleri olarak kodlamanın / kod çözmenin en verimli / deyimsel yolunun ne olduğunu bulmaya çalışıyorum.

[UInt8] kullanmak için bir cevap var , ancak Veri üzerinde bulamadığım çeşitli işaretçi API'leri kullanıyor gibi görünüyor.

Temel olarak şuna benzeyen bazı özel uzantılar istiyorum:

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

Benden gerçekten kaçan kısım, bir sürü belgeye baktım, herhangi bir temel yapıdan (tüm sayılar) nasıl bir tür işaretçi şeyi (OpaquePointer veya BufferPointer veya UnsafePointer?) C'de, sadece önüne bir "ve" işareti koyardım ve işte oraya.


Yanıtlar:


262

Not: Kod şu anda Swift 5 (Xcode 10.2) için güncellendi . (Swift 3 ve Swift 4.2 sürümleri düzenleme geçmişinde bulunabilir.) Ayrıca muhtemelen hizalanmamış veriler artık doğru şekilde işleniyor.

DataBir değerden nasıl yaratılır

Swift 4.2'den itibaren, veriler basitçe bir değerden oluşturulabilir:

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

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

Açıklama:

  • withUnsafeBytes(of: value) Kapanışı, değerin ham baytlarını kapsayan bir tampon işaretçisi ile çağırır.
  • Ham tampon işaretçisi bir bayt dizisidir, bu nedenle Data($0)verileri oluşturmak için kullanılabilir.

Bir değer nasıl alınır Data

Swift 5'ten itibaren, withUnsafeBytes(_:)of Data, kapanışı UnsafeMutableRawBufferPointerbaytlara "türsüz" olarak çağırır . load(fromByteOffset:as:)Yöntem, hafızadan değerini okur:

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

Bu yaklaşımla ilgili bir sorun var: Belleğin tür için özellik hizalı olmasını gerektirir (burada: 8 baytlık bir adrese hizalı). Ancak bu garanti edilmez, örneğin veriler başka bir Datadeğerden bir dilim olarak elde edilmişse .

Bu nedenle baytları şu değere kopyalamak daha güvenlidir :

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

Açıklama:

  • withUnsafeMutableBytes(of:_:) Kapanışı, değerin ham baytlarını kapsayan değiştirilebilir bir tampon işaretçisi ile çağırır.
  • copyBytes(to:)Yöntem DataProtocol(ki Datakopya uygundur) bu tampona verilerinden bayt.

Dönüş değeri, copyBytes()kopyalanan bayt sayısıdır. Hedef arabelleğin boyutuna eşittir veya veriler yeterli bayt içermiyorsa daha azdır.

Genel çözüm 1

Yukarıdaki dönüşümler artık aşağıdaki genel yöntemler olarak kolayca uygulanabilir 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
    }
}

Kısıtlama T: ExpressibleByIntegerLiteralburaya eklenir, böylece değeri kolayca "sıfır" olarak başlatabiliriz - bu gerçekten bir kısıtlama değildir, çünkü bu yöntem yine de "üç değerlikli" (tam sayı ve kayan nokta) türlerle kullanılabilir, aşağıya bakın.

Misal:

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

Benzer şekilde, dönüştürmek diziler için Datave arka:

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

Misal:

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]

Genel çözüm # 2

Yukarıdaki yaklaşımın bir dezavantajı vardır: Aslında sadece tamsayılar ve kayan nokta türleri gibi "önemsiz" türlerle çalışır. "Karmaşık" türleri gibi Array ve String(gizli) olması altında yatan depolama işaretçiler ve sadece yapı kendisi kopyalayarak etrafına geçirilen edilemez. Ayrıca sadece gerçek nesne depolamasına işaret eden referans türleriyle de çalışmaz.

Öyleyse bu sorunu çözebilirsiniz.

  • DataVe geri dönüştürme yöntemlerini tanımlayan bir protokol tanımlayın :

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
    
  • Dönüşümleri bir protokol uzantısında varsayılan yöntemler olarak uygulayın:

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

    Burada, sağlanan bayt sayısının türün boyutuyla eşleşip eşleşmediğini kontrol eden bir kullanılabilir başlatıcı seçtim .

  • Ve son olarak, güvenle Datave geri dönüştürülebilecek tüm türlere uygunluğu beyan edin :

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

Bu, dönüşümü daha da zarif hale getirir:

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

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

İkinci yaklaşımın avantajı, yanlışlıkla güvenli olmayan dönüşümler yapamayacağınızdır. Dezavantajı, tüm "güvenli" türleri açıkça listelemeniz gerektiğidir.

Protokolü, önemsiz olmayan bir dönüştürme gerektiren diğer türler için de uygulayabilirsiniz, örneğin:

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

veya gerekli olan her şeyi yapmak için dönüştürme yöntemlerini kendi türlerinize uygulayın, böylece bir değeri serileştirin ve serisini kaldırın.

Bayt sırası

Yukarıdaki yöntemlerde bayt sırası dönüşümü yapılmaz, veriler her zaman ana bilgisayar bayt sırasındadır. Platformdan bağımsız bir gösterim için (ör. "Big endian" aka "ağ" bayt sırası), karşılık gelen tam sayı özelliklerini kullanın. başlatıcılar. Örneğin:

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
}

Elbette bu dönüştürme genel olarak jenerik dönüştürme yöntemiyle de yapılabilir.


varBaşlangıç ​​değerinin bir kopyasını almamız gerektiği gerçeği , baytları iki kez kopyaladığımız anlamına mı geliyor? Şu anki kullanım durumumda, onları Veri yapılarına dönüştürüyorum, böylece appendonları artan bir bayt akışına çevirebiliyorum . Düz C'de, bu kadar kolaydır *(cPointer + offset) = originalValue. Böylece baytlar yalnızca bir kez kopyalanır.
Travis Griggs

1
@TravisGriggs: Bir int veya float'ı kopyalamak büyük olasılıkla alakalı olmayacaktır, ancak Swift'de benzer şeyler yapabilirsiniz . Eğer bir kartınız varsa, Swift kodunuza çok ptr: UnsafeMutablePointer<UInt8>benzeyen bir şey aracılığıyla referans belleğe atayabilirsiniz UnsafeMutablePointer<T>(ptr + offset).pointee = value. Olası bir sorun vardır: Bazı işlemciler yalnızca hizalanmış bellek erişimine izin verir , örneğin bir Int'yi tek bir bellek konumunda saklayamazsınız. Bunun şu anda kullanılan Intel ve ARM işlemciler için geçerli olup olmadığını bilmiyorum.
Martin R

1
@TravisGriggs: (devamı) ... Ayrıca bu, yeterince büyük bir Data nesnesinin zaten yaratılmış olmasını gerektirir ve Swift'de Data nesnesini yalnızca oluşturabilir ve başlatabilirsiniz , bu nedenle işlem sırasında ek bir sıfır bayt kopyasına sahip olabilirsiniz. başlatma. - Daha fazla ayrıntıya ihtiyacınız varsa, yeni bir soru göndermenizi öneririm.
Martin R

2
@HansBrende: Korkarım bu şu anda mümkün değil. Bir extension Array: DataConvertible where Element: DataConvertible. Swift 3'te bu mümkün değil ama Swift 4 için planlanmıştı (bildiğim kadarıyla). Github.com/apple/swift/blob/master/docs/… adresindeki
Martin R

1
@m_katsifarakis: Eğer yanlış yazmış olabilir misin Int.selfolarak Int.Type?
Martin R

3

Şunları kullanarak değiştirilebilir nesnelere güvenli olmayan bir işaretçi elde edebilirsiniz withUnsafePointer:

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

Değişmez nesneler için bir tane elde etmenin bir yolunu bilmiyorum, çünkü inout operatörü yalnızca değiştirilebilir nesneler üzerinde çalışır.

Bu, bağlantı kurduğunuz cevapta gösterilmiştir.


2

Benim durumumda, Martin R'nin cevabı yardımcı oldu ama sonuç tersine döndü. Bu yüzden kodunda küçük bir değişiklik yaptım:

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

Sorun LittleEndian ve BigEndian ile ilgilidir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.