In Swift, is there any issue to miss the completionHandler in a method with escaping?

huangapple go评论59阅读模式
英文:

In Swift, is there any issue to miss the completionHandler in a method with escaping?

问题

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

func getData(){
    testEscaping { data in
        print("我得到了数据")
    }
}

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

似乎print方法会卡住,因为在testEscaping方法中从未调用completionHandler。这是个问题还是应该没关系?

最初的想法是上述代码是否存在内存泄漏问题。为什么编译器不警告我呢?换句话说,当使用escaping时,我们是否需要非常小心确保在所有代码路径中调用completionHandler?如果代码逻辑复杂,我们该如何找到丢失的completionHandler

func testEscaping(completionHandler: @escaping (_ data: Data) -> ()) {
    guard { /* ... */ } else {
        // 易于知道要调用completionHandler
        completionHandler(nil)
        return
    }

    // ... 一些可能引发异常并在中途失败的复杂逻辑
    // ... 我们应该捕获所有可能的错误并调用completionHandler,还是可以遗漏completionHandler并将错误抛出?

    completionHandler(goodData)
}

------更新-----
感谢回答问题。我刚刚发现这个WWDC视频(https://developer.apple.com/videos/play/wwdc2021/10132/),讲解了同样的问题,我觉得非常有帮助。把它放在这里,以防其他人有同样的困惑。

英文:

In Swift, I am learning the method @escaping return type and I know it is for async calls. The question is: do we need to make sure the completionHandler is handled in all code paths? Consider the following code sample:

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

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

Seems like the print method will be stuck since the completionHandler is never being called in the testEscaping method. Is this an issue or it should be OK?

The initial thought was if the above code has some memory leak issue. Why the compiler doesn't warn me? In other words, do we need to be very careful to make sure the completionHandler is called in all code paths when using escapting? If the code logic is complex, how should we find the missing 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)
}

------Updated-----
Thanks for answering the question. I just found this WWDC video (https://developer.apple.com/videos/play/wwdc2021/10132/) that talked about the same and I found it is very helpful. Post it here in case someone else has the same confusion.

答案1

得分: 2

For escaping closures, no.
对于逃逸闭包,不需要。

For completion handlers in asynchronous methods, probably yes.
对于异步方法中的完成处理程序,可能是的。

The escaping closures aren't necessarily for asynchronous tasks. It merely indicates that the closure may outlive the lifetime of the callee. It can be stored as a property of callee, some global variable, etc. Since it, by itself, has nothing to do with asynchronous task, it doesn't make sense to warn for unhandled escaping closures. We don't even know which escaping closures are meant to be completion handlers!
逃逸闭包不一定用于异步任务。它仅表示该闭包可能超出调用者的生命周期。它可以存储为调用者的属性、某个全局变量等。由于它本身与异步任务无关,因此警告未处理的逃逸闭包没有意义。我们甚至不知道哪些逃逸闭包是用作完成处理程序的!

When it comes to asynchronous methods with completion handler, it's probably a good idea to call it once and only once on each possible execution path, as that's how Swift Concurrency async method works. If you start using new concurrency features and port existing completion-based asynchronous methods into async methods, calling it more than once will result in crash (assuming you're using CheckedContinuation), and not calling it will result in leak of the Task closure and variables it captures.
当涉及带有完成处理程序的异步方法时,最好在每个可能的执行路径上仅调用一次,因为这是 Swift 并发 async 方法的工作方式。如果您开始使用新的并发功能并将现有的基于完成的异步方法移植到 async 方法中,多次调用它将导致崩溃(假设您正在使用 CheckedContinuation),而不调用它将导致 Task 闭包及其捕获的变量泄漏。

Since @escaping indicates that the closure may outlive the context, it won't leak anything unless you actually make it outlive and somehow make a reason to be leaked. In case of example you provided, the closure has no reference to it after testEscaping finishes executing so it gets deallocated immediately.
由于 @escaping 表示该闭包 可能 超出上下文的生命周期,除非您确实使它超出上下文并某种方式使其泄漏,否则它不会泄漏任何内容。在您提供的示例中,testEscaping 执行完毕后,闭包不再有对它的引用,因此它会立即被销毁。

If the code logic is complex, how should we find the missing completionHandler ?
如果代码逻辑复杂,应该如何找到缺失的 completionHandler?

There is no simple answer to this. defer may help, but it's ultimately on implementer's hand.
这没有简单的答案。defer 可能会有所帮助,但最终取决于实现者的处理。

This is one of reasons why Swift came up with new async/await concept.
这就是为什么 Swift 推出了新的异步/等待概念之一。

It's quite easy to bail-out of the asynchronous operation early by simply returning without calling the correct completion-handler block. When forgotten, the issue is very hard to debug — https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md#problem-4-many-mistakes-are-easy-to-make

With new async method, completion of asynchronous task is expressed by returning the method. Just like you can't miss returning in synchronous methods, you can't miss returning in asynchronous methods.

这很容易通过简单地返回而不调用正确的完成处理程序块来提前退出异步操作。一旦被遗忘,这个问题就很难调试 — https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md#problem-4-many-mistakes-are-easy-to-make

使用新的异步方法,异步任务的完成通过返回方法来表示。就像您不能在同步方法中漏掉返回一样,在异步方法中也不能漏掉返回。

英文:

> do we need to make sure the completionHandler is handled in all code paths?

For escaping closures, no.

For completion handlers in asynchronous methods, probably yes.

The escaping closures aren't necessarily for asynchronous tasks. It merely indicates that the closure may outlive the lifetime of the callee. It can be stored as a property of callee, some global variable, etc. Since it, by itself, has nothing to do with asynchronous task, it doesn't make sense to warn for unhandled escaping closures. We don't even know which escaping closures are meant to be completion handlers!

When it comes to asynchronous methods with completion handler, it's probably a good idea to call it once and only once on each possible execution path, as that's how Swift Concurrency async method works. If you start using new concurrency features and port existing completion-based asynchronous methods into async methods, calling it more than once will result in crash (assuming you're using CheckedContinuation), and not calling it will result in leak of the Task closure and variables it captures.

> The initial thought was if the above code has some memory leak issue.

Since @escaping indicates that the closure may outlive the context, it won't leak anything unless you actually make it outlive and somehow make a reason to be leaked. In case of example you provided, the closure has no reference to it after testEscaping finishes executing so it gets deallocated immediately.

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

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

> If the code logic is complex, how should we find the missing completionHandler ?

There is no simple answer to this. defer may help, but it's ultimately on implementer's hand.

This is one of reasons why Swift came up with new async/await concept.

> It's quite easy to bail-out of the asynchronous operation early by simply returning without calling the correct completion-handler block. When forgotten, the issue is very hard to debug — https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md#problem-4-many-mistakes-are-easy-to-make

With new async method, completion of asynchronous task is expressed by returning the method. Just like you can't miss returning in synchronous methods, you can't miss returning in asynchronous methods.

答案2

得分: 1

回答你的问题,根据你的用例,如果你的 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 语句之后的作用域末尾时,defer 语句才会执行。

如果控制在第一个 guard 语句内部达到作用域末尾,defer 语句不会执行。这是因为 guard 语句在 defer 语句之前定义。

希望这有所帮助。

英文:

To answer your question, depending on your use case, it could be bad if your testEscaping method didn't invoke the completion handler on all paths. Assuming it's an async method, the caller may never be notified that testEscaping completed.

I personally use defer statements for this sort of thing. A defer will ensure that certain code will run at the end of a scope (such as a method, closure, do block, loop, etc).

Here's an example:

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

func testEscaping(completionHandler: @escaping (Result&lt;Data, Error&gt;) -&gt; ()) {

    someAsyncMethod { data in

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

        var result: Result&lt;Data, Error&gt; = .failure(.unknown)

        defer {
            completionHandler(result)
        }

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

        guard data.count &lt; 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(&quot;Error&quot;, error)
            result = .failure(error)
        } catch {
            print(&quot;Other Error&quot;, error)
            result = .failure(.other(error))
        }
    }
}

Notice how I defined an Error enum that can be used along with Swift's built in Result type. That way we can pass more helpful info to the caller if something goes wrong.

The completion handler is only called in two places. Right after the isReady conditional and inside the defer statement. I did this to illustrate that the defer statement will only execute if control reaches the end of scope after the defer statement is defined.

The defer statement will not execute if control reaches the end of scope inside the first guard statement. This is because the guard statement is defined before the defer statement.

Hope this helps.

huangapple
  • 本文由 发表于 2023年5月7日 05:57:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76191344.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定