【问题标题】:Unwrapping optional value (returned by data.withUnsafeBytes(_:)) sometimes does not work with guard let展开可选值(由 data.withUnsafeBytes(_:) 返回)有时不适用于 guard let
【发布时间】:2018-10-26 00:54:21
【问题描述】:

我对 guard let 语句有疑问,它的行为很奇怪。整个代码如下。即使 readDatasizeOfData 不是 nil,方法 readActivity(subdata: Data) 中的其他语句块 guard let data = readData, let size = sizeOfData else ... 也会错误地执行。

代码

import Foundation

enum ActivityDataReaderError: Error {
    case activityIsReadingOtherCentral
    case bluetooth(Error?)
    case staleData
}

protocol ActivityDataReaderDelegate: class {
    func didReadActivity(data: Data)
    func didFailToReadActivity(error: ActivityDataReaderError)
}

final class ActivityDataReader {
    private var sizeOfData: Int?
    private var isOtherDeviceReading: Bool {
        // 0xFFFF
        return sizeOfData == 65535
    }
    private var readData: Data?

    var isEmpty: Bool {
        return sizeOfData == nil
    }

    weak var delegate: ActivityDataReaderDelegate?

    static func timestampValue(_ timestamp: UInt32) -> Data {
        var value = timestamp
        return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func reset() {
        readData = nil
        sizeOfData = nil
        NSLog("reset() -- \(Thread.current)")
    }

    func readActivity(data: Data?, error: Error? = nil) {
        guard let data = data else {
            delegate?.didFailToReadActivity(error: .bluetooth(error))
            return
        }
        let isFirstChunk = readData == nil
        if isFirstChunk {
            let sizeData = data.subdata(in: 0..<2)
            sizeOfData = sizeData.withUnsafeBytes { $0.pointee }
            guard !isOtherDeviceReading else {
                delegate?.didFailToReadActivity(error: .activityIsReadingOtherCentral)
                return
            }
            NSLog(String("readActivity() Size of data: \(String(describing: sizeOfData))"))
            let subdata = data.subdata(in: 2..<data.count)
            readActivity(subdata: subdata)
        } else {
            readActivity(subdata: data)
        }
    }

    private func readActivity(subdata: Data) {
        if let lastReadData = readData {
            readData = lastReadData + subdata
        } else {
            readData = subdata
        }
        guard let data = readData, let size = sizeOfData else {
            NSLog("WTF? data:\(String(describing: readData)), "
                + "sizeOfData: \(String(describing: sizeOfData)), "
                + "thread: \(Thread.current)")
            assertionFailure("WTF")
            return
        }
        NSLog("subdata: \(String(describing: subdata)), "
            + "totalReadBytes: \(data.count), "
            + "size: \(size)")
        if data.count == size {
            delegate?.didReadActivity(data: data)
            reset()
        }
    }
}

测试

由于assertionFailure("WTF"),有时会通过有时会崩溃的测试。

class ActivityDataServiceReaderTests: XCTestCase {
    var service: ActivityDataReader?

    override func setUp() {
        super.setUp()
        service = ActivityDataReader()
    }

    override func tearDown() {
        service = nil
        super.tearDown()
    }

    func testBufferIsNotEmpty() {
        NSLog("testBufferIsNotEmpty thread: \(Thread.current)")
        guard let service = service else { fatalError() }
        let firstDataBytes = [UInt8.min]
        let data1 = Data(bytes: [7, 0] + firstDataBytes)
        service.readActivity(data: data1)
        XCTAssertFalse(service.isEmpty)

        service.reset()
        XCTAssertTrue(service.isEmpty)
    }
}

崩溃时的控制台日志

2018-10-25 14:53:30.033573+0200 GuardBug[84042:11188210] WTF? data:Optional(1 bytes), sizeOfData: Optional(7), thread: <NSThread: 0x600003399d00>{number = 1, name = main}

环境

  • Xcode10
  • 带有旧版构建系统的 swift 4.1
  • swift 4.2

在我看来,在方法 readActivity(subdata: Data) 的 guard let else 块中执行 else 块中的代码是不可能的。一切都在主线程上运行。我错过了什么吗?怎么可能有时测试通过有时崩溃?

感谢您的帮助。

编辑:

guard let + data.withUnsafeBytes 更窄的问题:

func testGuardLet() {
    let data = Data(bytes: [7, 0, UInt8.min])
    let sizeData = data.subdata(in: 0 ..< 2)
    let size: Int? = sizeData.withUnsafeBytes { $0.pointee }
    guard let unwrappedSize = size else {
        NSLog("failure: \(size)")
        XCTFail()
        return
    }
    NSLog("success: \(unwrappedSize)")
}

日志:

2018-10-25 16:32:19.497540+0200 GuardBug[90576:11351167] failure: Optional(7)

【问题讨论】:

  • 尝试选择性地分别包装每个值,并在发生崩溃时检查每个值的日志
  • 欢迎来到 StackOverflow。请在您的问题中仅包含相关代码。不要只是转储整个课程,并让人们阅读与您所要求的内容无关的代码。请阅读minimal reproducible exampleedit 你的问题以显示演示问题的最小代码。
  • 我在这里看到的唯一可能导致类似您所描述的事情的事情似乎是 sizeOfBytes 的竞争条件。当您在readActivity(data:error:) 中分配给它时,您正在使用闭包。我建议在didSet 中为 sizeOfBytes 写一条日志语句,看看这是否真的是原因。
  • @jlowe didSet 在主线程上被调用,所以这不是由竞争条件引起的。
  • @Tobi @jlowe 当我尝试分别解开每个值时,问题在于sizeOfData。这导致执行else 块,但在日志中我可以看到它的值不是nil

标签: ios swift optional


【解决方案1】:

感谢https://forums.swift.org/t/unwrapping-value-with-guard-let-sometimes-does-not-work-with-result-from-data-withunsafebytes-0-pointee/17357 的帮助,问题出在线路上:

let size: Int? = sizeData.withUnsafeBytes { $0.pointee }

其中读取的数据被向下转换为 Optional Int(8 字节长),但 sizeData 它本身只有 2 字节长。我不知道它有时是如何工作的,但解决方案——似乎工作正常——是以休闲方式使用方法withUnsafeBytes

let size = sizeData.withUnsafeBytes { (pointer: UnsafePointer<UInt16>) in pointer.pointee }

返回值不是可选的,并且具有正确的 UInt16 类型(2 个字节长)。

【讨论】:

    【解决方案2】:

    如果勾选Documentation,有警告:

    警告不应存储和使用字节指针参数 在调用闭包的生命周期之外。

    似乎你应该处理闭包体内的大小

    func testGuardLet() {
            let data = Data(bytes: [7, 0, UInt8.min])
            var sizeData = data.subdata(in: 0 ..< 2)
            withUnsafeBytes(of: &sizeData) { bytes in
                print(bytes.count)
                for byte in bytes {
                    print(byte)
                }
            }
    
            let bytes = withUnsafeBytes(of: &sizeData) { bytes in
                return bytes // BUGS ☠️☠️☠️
            }
        }
    

    【讨论】:

    • 使用Clean Build Folder 尝试几次,即使是打印。它有时也会失败。 (我正在用 XCTest 试试)
    • 错过了一件事。更新我的答案。
    • 问题不在于variable declared in 'guard' condition is not usable in its body。在示例代码中,else 块内的任何地方都没有使用未包装的值。问题是sizeData.withUnsafeBytes { $0.pointee } 分配的值是guard let nil,但在 else 块内部的值是7
    • @MichalRaška,很抱歉在第一个答案中误导。回答得太快了。请检查更新。
    • 如果我理解得很好,您的建议正是如此,文档中所说的不应该这样做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-20
    • 1970-01-01
    • 1970-01-01
    • 2021-05-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多