不理解此代码中的完成处理程序
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)
)
那么你知道函数参数是如何接受String
s、Int
s 等的吗?
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
闭包,如我的回答开头所示。这就是典型的闭包例子。
【讨论】:
以上是关于不理解此代码中的完成处理程序的主要内容,如果未能解决你的问题,请参考以下文章