在Swift中,是否有任何问题会在一个带有escaping的方法中错过completionHandler?

0lvr5msh  于 2023-05-16  发布在  Swift
关注(0)|答案(2)|浏览(127)

在Swift中,我正在学习@escaping方法的返回类型,我知道它是用于异步调用的。问题是:我们是否需要确保completionHandler在所有代码路径中被处理?请考虑以下代码示例:

func getData(){
    testEscaping { data in
        print("I get the data")
    }
}

func testEscaping(completionHandler: @escaping (_ data: Data) -> ()) {
    return;
}

print方法似乎会卡住,因为testEscaping方法中从未调用completionHandler。这是一个问题还是应该没事?
最初的想法是上面的代码是否有一些内存泄漏问题。为什么编译器不警告我?换句话说,我们是否需要非常小心地确保在使用escapting时在所有代码路径中调用completionHandler?如果代码逻辑很复杂,我们应该如何找到丢失的completionHandler

func testEscaping(completionHandler: @escaping (_ data: Data) -> ()) {
    guard { /* ... */ } else {
        // easy to know to call completionHandler
        completionHandler(nil)
        return
    }

    // ... some complex logic which might cause exceptions and fail at the middle
    // ... should we catch all possbile errors and call completionHandler or it should OK 
    // ... miss the completionHandler and throw the error out?

    completionHandler(goodData)
}
brvekthn

brvekthn1#

我们是否需要确保completionHandler在所有代码路径中都被处理?
对于逃逸闭包,没有。
对于异步方法中的完成处理程序,可能是的。
转义闭包不一定用于异步任务。它仅仅表明闭包的生存期可能超过被调用方的生存期。它可以存储为被调用方的属性,一些全局变量等。由于它本身与异步任务无关,因此对未处理的转义闭包发出警告是没有意义的。我们甚至不知道哪些转义闭包是完成处理程序!
当涉及到带有完成处理程序的异步方法时,在每个可能的执行路径上调用一次且仅一次可能是一个好主意,因为这就是Swift Concurrency async方法的工作方式。如果你开始使用新的并发特性,并将现有的基于完成的异步方法移植到async方法中,多次调用它将导致崩溃(假设你使用的是CheckedContinuation),而不调用它将导致Task闭包和它捕获的变量泄漏。
最初的想法是上面的代码是否有一些内存泄漏问题。
由于@escaping表明闭包 * 可能 * 比上下文更持久,因此它不会泄漏任何内容,除非您实际上使其更持久并以某种方式制造泄漏的原因。在您提供的示例中,闭包在testEscaping执行完成后没有对其的引用,因此它会立即被释放。

func getData(){
    testEscaping { data in
        print("I get the data")
    }
}

func testEscaping(completionHandler: @escaping (_ data: Data) -> ()) {
    return;
}

如果代码逻辑很复杂,我们应该如何找到缺少的completionHandler?
这个问题没有简单的答案。defer可能会有所帮助,但最终还是要由实现者来决定。
这也是Swift提出async/await概念的原因之一。
通过简单地返回而不调用正确的完成处理程序块,可以很容易地提前退出异步操作。如果忘记了,这个问题很难调试-https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md#problem-4-many-mistakes-are-easy-to-make
使用新的async方法,异步任务的完成通过返回方法来表示。就像在同步方法中不能错过返回一样,在异步方法中也不能错过返回。

s2j5cfk0

s2j5cfk02#

为了回答您的问题,根据您的用例,如果您的testEscaping方法没有在所有路径上调用完成处理程序,那么情况可能会很糟糕。假设它是一个异步方法,调用者可能永远不会收到testEscaping完成的通知。
我个人使用defer语句来处理这类事情。defer将确保某些代码将在范围的末尾运行(例如方法,闭包,do块,循环等)。
下面是一个例子:

enum Error: Swift.Error {
    case dataMissing
    case dataTooLarge
    case notReady
    case `internal`
    case unknown
    case other(Swift.Error)
}

func testEscaping(completionHandler: @escaping (Result<Data, Error>) -> ()) {

    someAsyncMethod { data in

        guard isReady else {
            return completionHandler(.failure(.notReady))
        }

        var result: Result<Data, Error> = .failure(.unknown)

        defer {
            completionHandler(result)
        }

        guard let data else {
            return { result = .failure(.dataMissing) }()
        }

        guard data.count < 100 else {
            return { result = .failure(.dataTooLarge) }()
        }

        do {
            let finalData = try self.someThrowingMethod(data: data)
            if finalData.first == 0 {
                result = .success(finalData)
            }
        } catch let error as Error {
            print("Error", error)
            result = .failure(error)
        } catch {
            print("Other Error", error)
            result = .failure(.other(error))
        }
    }
}

注意我是如何定义一个Error枚举的,它可以与Swift内置的Result类型沿着使用。这样,如果出现问题,我们可以向呼叫者传递更多有用的信息。
完成处理程序只在两个地方被调用。就在isReady条件语句之后和defer语句内部。我这样做是为了说明defer语句只有在定义了defer语句之后控制到达作用域末尾时才会执行。
如果控制到达第一个guard语句内的作用域末尾,则不会执行defer语句。这是因为guard语句在defer语句之前定义。
希望这能帮上忙。

相关问题