不理解此代码中的完成处理程序

Posted

技术标签:

【中文标题】不理解此代码中的完成处理程序【英文标题】:Not understanding the completion handler in this code 【发布时间】:2020-09-28 03:43:32 【问题描述】:

“完成(项目)”的目的是什么?我知道这些是在我们不知道下载时间等操作何时完成时使用的。但是,在这种情况下 loadData() 只是从项目目录中提取一个 plist 文件,所以我觉得这是一个恒定的时间,并且不需要完成处理程序。另外,我的教科书说它返回注释数组,但我没有看到任何返回语句。我是 swift 新手,所以如果这不是一个好问题,我深表歉意。

func fetch(completion: (_ annotations: [RestaurantItem]) -> ()) 
        if items.count > 0  items.removeAll() 
        for data in loadData() 
            items.append(RestaurantItem(dict: data))
        
        completion(items)
    

【问题讨论】:

是的,这很愚蠢,但在技术上是正确的(因为它可以编译和运行),Swift 代码。它不像教科书应该向您介绍的那种代码。而且,正如您所建议的,它不返回数组;它将一个数组传递给完成处理程序。您可能会发现这种代码的唯一地方是在单元测试中(即使那样,它也会有点冒险)。如果这是来自教科书,那么您可能学习的课程并不理想。 我读这个的方式...函数 fetch 是用一个参数调用的。该参数的名称是“完成”。该参数是一个闭包,它将一个 RestaurantItem 数组作为其输入,并且不返回任何内容。该函数用 RestaurantItem's 填充数组项,然后调用行完成(items),用它刚刚填充的数组调用该闭包。 @RobNapier 如果这不是来自教科书,或者如果教科书不是关于完成处理程序的,我会说代码不一定是愚蠢的。我有时会编写这样的代码,以允许我将来将函数更改为异步函数,而不会破坏调用者。这是一种面向未来的方式。 @Sweeper 即使你要这样做(是的,我有,但肯定不是在教育代码中),IMO 你应该把它写成DispatchQueue.async,否则你打电话给在函数返回之前完成处理程序,这会鼓励微妙的计时错误或微妙的计时依赖性。 【参考方案1】:

是的,带有完成处理程序的函数不需要返回语句——相反,只需调用完成处理程序 (completion(items))

那么你知道函数参数是如何接受Strings、Ints 等的吗?

func doSomething(inputThing: Int) 
                             ^ this is the type (an Int)

他们也可以接受闭包。在您的示例中,completion 参数接受一个闭包。

func fetch(completion: (_ annotations: [RestaurantItem]) -> ()) 
                       ^ this is the type (a closure)

闭包基本上是可以传递的代码块。通常,如果一个函数接受一个闭包作为参数,您将闭包称为“完成处理程序”(因为它通常会在函数结束时调用)。

您的闭包还指定了[RestaurantItem] 类型的输入和() (Void) 的输出(无效,因为闭包本身不会返回任何内容)。 _ annotations: 部分是不必要的:只需这样做:

func fetch(completion: ([RestaurantItem]) -> ()) 

当你调用函数时,你需要传入一个闭包,并将输入分配给一个变量。

fetch(completion:  restaurantItems in
    /// do something with restaurantItems (assigned to the input)
)

您将在func fetch(completion: (_ annotations: [RestaurantItem]) -> ()) 的末尾调用此闭包。

func fetch(completion: (_ annotations: [RestaurantItem]) -> ()) 
    if items.count > 0  items.removeAll() 
    for data in loadData() 
        items.append(RestaurantItem(dict: data))
    
    completion(items) /// call the closure!
    /// this is a completion handler because you called it at the end of the function

调用completion(items)items 传递给闭包的输入,该闭包被分配给restaurantItems

通常闭包用于需要时间运行的函数,例如下载文件。但在您的示例中,loadData() 看起来会立即发生,因此您应该使用具有返回类型的普通函数。

func fetch() -> [RestaurantItem] 
    if items.count > 0  items.removeAll() 
    for data in loadData() 
        items.append(RestaurantItem(dict: data))
    
    return items


let restaurantItems = fetch()

【讨论】:

感谢详细的回复!问题 - 完成的实施在哪里?您说它已分配给 restaurantItems,但我看不到它分配给 RestaurantItems 的位置。 你没有明确地说像restaurantItems = items这样的话。所以函数fetch接受一个闭包作为参数,闭包的类型是([RestaurantItem]) -> ()。你传入的闭包是 restaurantItems in ... ——所以restaurantItems将是输入。然后,当您在fetch 中调用闭包时,您将items 插入到输入中,即restaurantItems。把闭包想象成函数——restaurantItems 是参数,你传入items 好的,所以项目被传递到闭包中,但它不返回任何东西,那么当项目被传递到闭包时,闭包到底在做什么? 这取决于你!用restaurantItems 做任何你想做的事,例如,像@Rob 下面所说的那样更新一个tableview。在我的回答中,将/// do something with restaurantItems (assigned to the input) 替换为您想做的任何事情。 但是在完成(项目)的情况下,那到底是在做什么?将项目传递给闭包后......根据我给出的代码发生了什么?对不起,如果我对闭包的理解不正确。【参考方案2】:

我们通常在编写异步代码时使用完成处理程序闭包,即在我们开始一些耗时的事情(例如网络请求)但您不想阻塞调用者(通常是主线程)的情况下这种相对较慢的网络请求正在发生。

那么,让我们看一下典型的完成处理程序模式。假设您正在使用URLSession 进行异步网络请求:

func fetch(completion: @escaping ([RestaurantItem]) -> Void) 
    let task = URLSession.shared.dataTask(with: url)  data, response, error in
        // parse the `data`
        let items: [RestaurantItem] = ...
        DispatchQueue.async  completion(items) 
    
    task.resume()

(我使用 URLSession 作为异步过程的示例。显然,如果您使用 Alamofire 或 Firebase 或任何异步 API,则想法是相同的。我们调用完成处理程序闭包 completion,当异步请求完成。)

这会启动网络请求,但会立即返回,稍后网络请求完成时会调用completion。注意,fetch 不应该直接更新模型。它只是将结果提供给闭包。

您的调用者(可能是视图控制器)负责在稍后调用 completion 闭包时更新模型和 UI:

var items: [RestaurantItems] = []   // start with empty array

override func viewDidLoad() 
    super.viewDidLoad()

    fetch  items in
        print("got items", items)
        self.items = items          // this is where we update our model
        self.tableView.reloadData() // this is where we update our UI, a table view in this example
    
    print("finishing viewDidLoad")

如果我们观察我们的控制台,我们将看到“完成 viewDidLoad”消息“得到项目”消息之前。但是我们提供给 fetch 的闭包会更新模型并触发 UI 的重新加载。

这是一个过于简化的例子,但这是完成处理程序闭包的基本思想,允许我们提供一个可以在某些异步任务完成时执行的代码块,同时允许 fetch 立即返回,以便我们不会阻止 UI。

但是,我们经历这种复杂的闭包模式的唯一原因是fetch 执行的任务是异步运行的。如果fetch 没有做异步操作(在您的示例中似乎没有这样做),我们根本不会使用这种闭包模式。您只需return 结果即可。


那么,让我们回到你的例子。

有几个问题:

    更新items 并返回结果(无论是直接返回还是使用闭包)是没有意义的。你会做一个或另一个,但不能两者兼而有之。所以,我可能会建议你创建一个局部变量,然后在闭包中传递结果(很像我上面的异步模式)。例如:

    func fetch(completion: (_ annotations: [RestaurantItem]) -> ()) 
        var items: [RestaurantItem] = []
        for data in loadData() 
            items.append(RestaurantItem(dict: data))
        
        completion(items)
    
    

    我可能会使用map 进一步简化这一点,例如:

    func fetch(completion: (_ annotations: [RestaurantItem]) -> ()) 
        let items = loadData().map  RestaurantItem(dict: $0)) 
        completion(items)
    
    

    无论你做了以上哪一个,你都可以做到:

    func viewDidLoad() 
        ...
        fetch  items in
            self.items = items
        
    
    

    但这是非常具有误导性的。如果您看到一个名称为 fetch 的方法带有闭包,那么未来的读者只会认为它是一个异步方法(因为这是我们采用该模式的唯一原因)。如果它是同步的,我会将其简化为 return 结果:

    func fetch() -> [RestaurantItem] 
        return loadData().map  RestaurantItem(dict: $0)) 
    
    

    func viewDidLoad() 
        ...
        items = fetch()
    
    

不用说,如果fetch 是异步的,那么您使用@escaping 闭包,如我的回答开头所示。这就是典型的闭包例子。

【讨论】:

以上是关于不理解此代码中的完成处理程序的主要内容,如果未能解决你的问题,请参考以下文章

我应该写啥完成处理程序?

深入理解程序的结构

Alamofire 完成处理程序问题

Swift @escaping 和完成处理程序

快速完成处理程序

从地址获取坐标的完成处理程序