데이터와의 왕복 스위프트 번호 유형


95

Swift 3 Data대신[UInt8] 다양한 숫자 유형 (UInt8, Double, Float, Int64 등)을 데이터 객체로 인코딩 / 디코딩하는 가장 효율적이고 관용적 인 방법을 알아 보려고합니다.

있다 [UINT8]을 사용하여이 대답은 하지만, 내가 데이터에서 찾을 수없는 다양한 포인터 API를 사용하고있는 것으로 보인다.

기본적으로 다음과 같은 사용자 지정 확장을 원합니다.

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

정말 나를 피하는 부분은 여러 문서를 살펴 보았는데, 기본 구조체 (모든 숫자)에서 일종의 포인터 (OpaquePointer 또는 BufferPointer 또는 UnsafePointer?)를 얻을 수있는 방법입니다. C에서는 그 앞에 앰퍼샌드를 두드리면됩니다.


답변:


259

참고 : 코드는 현재 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에서 withUnsafeBytes(_:)of DataUnsafeMutableRawBufferPointer바이트에 "untyped" 로 클로저를 호출합니다 . 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:_:) 값의 원시 바이트를 덮는 가변 버퍼 포인터로 클로저를 호출합니다.
  • copyBytes(to:)방법 DataProtocol(처 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이 추가되어 값을 "0"으로 쉽게 초기화 할 수 있습니다.이 메서드는 어쨌든 "trival"(정수 및 부동 소수점) 유형과 함께 사용할 수 있기 때문에 실제로 제한이 아닙니다. 아래를 참조하십시오.

예:

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

위의 접근 방식에는 한 가지 단점이 있습니다. 실제로 정수 및 부동 소수점 유형과 같은 "사소한"유형에서만 작동합니다. "복잡한"유형 좋아 Array 하고String (숨겨진) 한 기본 스토리지에 대한 포인터 단지 구조체 자체를 복사하여 주위에 통과 할 수 없습니다. 또한 실제 객체 저장소에 대한 포인터 인 참조 유형에서는 작동하지 않습니다.

그러니 그 문제를 해결하세요.

  • 변환 방법을 정의하는 프로토콜을 정의하십시오 Data.

    protocol DataConvertible {
        init?(data: Data)
        var data: Data { get }
    }
  • 프로토콜 확장에서 기본 메소드로 변환을 구현하십시오.

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

    제공된 바이트 수가 유형의 크기와 일치하는지 확인 하는 실패 할 수있는 이니셜 라이저를 여기에서 선택했습니다 .

  • 마지막으로 안전하게 변환 할 수있는 모든 유형에 대한 적합성을 선언합니다 Data.

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

또는 값을 직렬화 및 역 직렬화하기 위해 필요한 모든 작업을 수행하기 위해 자신의 유형으로 변환 메서드를 구현합니다.

바이트 순서

위의 방법에서는 바이트 순서 변환이 수행되지 않으며 데이터는 항상 호스트 바이트 순서입니다. 플랫폼 독립적 표현 (예 : "빅 엔디안"일명 "네트워크"바이트 순서)의 경우 해당 정수 속성 resp를 사용합니다. 이니셜 라이저. 예를 들면 :

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
}

물론이 변환은 일반적인 변환 방법으로도 일반적으로 수행 할 수 있습니다.


var초기 값을 복사 해야한다는 사실 은 바이트를 두 번 복사한다는 것을 의미합니까? 현재 사용 사례에서는 데이터 구조를 데이터 구조로 변환 append하여 증가하는 바이트 스트림으로 변환 할 수 있습니다. 스트레이트 C에서는 *(cPointer + offset) = originalValue. 따라서 바이트는 한 번만 복사됩니다.
Travis Griggs

1
@TravisGriggs : int 또는 float를 복사하는 것은 아마도 관련이 없을 것입니다.하지만 Swift에서 비슷한 일을 할 수 있습니다 . 있는 경우 Swift 코드에 가장 가까운 ptr: UnsafeMutablePointer<UInt8>것을 통해 참조 된 메모리에 할당 할 수 있습니다 UnsafeMutablePointer<T>(ptr + offset).pointee = value. 한 가지 잠재적 인 문제가 있습니다. 일부 프로세서는 정렬 된 메모리 액세스 만 허용합니다 . 예를 들어 이상한 메모리 위치에 Int를 저장할 수 없습니다. 이것이 현재 사용되는 Intel 및 ARM 프로세서에 적용되는지 모르겠습니다.
Martin R

1
@TravisGriggs : (계속) ... 또한 충분히 큰 Data 객체가 이미 생성되어 있어야하며 Swift 에서는 Data 객체 만 생성 하고 초기화 할 수 있으므로 작업 중에 0 바이트의 추가 사본이있을 수 있습니다. 초기화. – 더 자세한 정보가 필요하면 새 질문을 게시하는 것이 좋습니다.
Martin R

2
@HansBrende : 현재 불가능합니다. 그것은 필요하다 extension Array: DataConvertible where Element: DataConvertible. 스위프트 3에서는 불가능하지만 스위프트 4 (내가 아는 한)에서는 가능하다. github.com/apple/swift/blob/master/docs/
Martin R

1
@m_katsifarakis : 당신이 잘못 입력 Int.self했을 수 Int.Type있습니까?
Martin R

3

다음 을 사용하여 변경 가능한 객체에 대한 안전하지 않은 포인터를 얻을 수 있습니다 withUnsafePointer.

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

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과 관련이 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.