前言async-await是WWDC2021期间Swift5.5结构化并发更改的一部分。Swift中的并发意味着允许多段代码同时运行。这是一个非常简化的描述,但它应该让您了解Swift中的并发性对应用程序性能的重要性。使用新的async方法和await语句,我们可以定义方法来执行异步工作。您可能已经阅读了ChrisLattner几年前出版的ChrisLattner的SwiftConcurrencyManifesto[1]。Swift社区中的许多开发人员对定义异步代码的结构化方式的未来感到兴奋。现在终于来了,我们可以使用async-await来简化我们的代码,让我们的异步代码更易于阅读。什么是异步?async表示异步的意思,可以看做是一个属性,明确表示一个方法进行的是异步工作。这种方法的一个示例如下所示:funcfetchImages()asyncthrows->[UIImage]{//..performdatarequest}fetchImages方法被定义为异步的并且可以抛出异常,这意味着它正在执行一个失败的的异步作业。如果一切顺利,该方法返回图像数组,如果出现问题,则抛出错误。异步如何替换完成回调闭包异步方法替换了常见的完成回调。完成回调在Swift中很常见,用于从异步任务返回,通常与result类型的参数结合使用。上面的方法一般会这样写:funcfetchImages(completion:(Result<[UIImage],Error>)->Void){//..执行数据请求}在今天的Swift版本中,completion闭包用于定义方法仍然可行,但它有一些异步刚好解决的缺点。您必须确保在每个可能的退出方法上调用完成闭包。如果不这样做,可能会导致应用程序无休止地等待结果。闭包代码更难阅读。与结构化并发相比,推理执行顺序并不容易。需要使用弱引用来避免循环引用。实现者需要打开结果才能得到结果。不能从实现级别使用trycatch语句。这些缺点是基于使用相对较新的Result枚举的闭包版本。很可能许多项目仍然使用完成回调而不是这个枚举:funcfetchImages(completion:([UIImage]?,Error?)->Void){//..performdatarequest}定义一个这样的方法来使它是我们很难推断调用方的结果。value和error都是可选的,这需要我们在任何情况下都进行解包。展开这些可选值会导致代码更加混乱,这对可读性没有帮助。什么是等待?await是用于调用异步方法的关键字。你可以把它们(async-await)看作是Swift中最好的朋友,因为一个永远不会离开另一个,你基本上可以说:“await正在等待他的伙伴async的回调”虽然这听起来很幼稚,但它是没有骗人!我们可以通过调用我们之前定义的异步方法fetchImages方法来查看示例:error\(error)")}可能很难相信,但上面的代码示例正在执行一个异步任务。使用await关键字,我们告诉我们的程序等待fetchImages方法的结果,并且只有在结果到达时才继续。这可能是一个图像集合,也可能是在获取图像时出错的错误。什么是结构化并发?具有异步等待方法调用的结构化并发使得对执行顺序的推理更加容易。方法是线性执行的,不会像闭包那样来回走动。为了更好地解释这一点,我们可以看看我们在结构化并发之前如何调用上面的代码示例://1.调用此方法fetchImages{resultin//3.异步方法内容返回switch结果{case.success(letimages):print("Fetched\(images.count)images.")case.failure(leterror):print("Fetchingimagesfailedwitherror\(error)")}}//2.调用方法结束如您所见,调用方法在获取图像之前结束。最终,我们收到了一个结果,我们又回到了完成回调的流程中。这是一个非结构化的执行顺序,可能难以遵循。如果我们在完成回调中执行另一个异步方法,这无疑会增加另一个闭包回调://1.调用此方法fetchImages{resultin//3.异步方法内容返回switchresult{case.success(letimages):print("Fetched\(images.count)images.")//4.调用调整大小方法resizeImages(images){resultin//6.Resize方法返回switch结果{case.success(letimages):print("Decoded\(images.count)images.")case.failure(leterror):print("Decodingimagesfailedwitherror\(error)")}}//5.图像获取方法返回case.failure(leterror):print("Fetchingimagesfailedwitherror\(error)")}}//2.每个闭包都会在调用方法的末尾加一层缩进,这样我们更难理解顺序的执行。结构化并发的作用最好通过使用async-await重写上面的代码示例来解释。do{//1.调用该方法letimages=tryawaitfetchImages()//2.图像获取方法返回//3.调用resize方法letresizedImages=tryawaitresizeImages(images)//4.Resize方法returnsprint("Fetched\(images.count)images.")}catch{print("Fetchingimagesfailedwitherror\(error)")}//5.调用方法结束执行的顺序是线性的,所以很容易理解,也很容易推理。当我们有时也执行复杂的异步任务时,理解异步代码会更容易。调用异步方法在不支持并发的函数中调用异步方法第一次使用async-await时可能会遇到这个错误。当我们尝试从不支持并发的同步调用环境中调用异步方法时,会出现此错误。我们也可以通过将我们的fetchData方法定义为异步来修复此错误:.相反,我们可以使用Task.init方法从一个新的支持并发的任务中调用一个异步方法,并将结果分配给我们视图模型中的一个属性:]funcfetchData(){Task.init{do{self.images=tryawaitfetchImages()}catch{//..handleerror}}}}使用带有尾随闭包的异步方法,我们创建了一个环境,其中In这个环境我们可以调用异步方法。一旦调用异步方法,获取数据的方法将返回,之后所有异步回调将在闭包内发生。在现有项目中使用async-await在现有项目中使用async-await时,您必须小心不要一次性破坏所有代码。在进行如此大规模的重构时,最好考虑暂时维护旧的实现,这样您就不必更新所有代码,直到您知道新的实现是否足够稳定为止。这类似于SDK中许多不同的开发人员和项目使用的弃用方法。显然,您不是必须这样做,但它可以让您更轻松地在项目中尝试使用async-await。除此之外,Xcode使重构代码变得非常容易,并且还提供了创建单个异步方法的选项:每个重构方法都有自己的目的并导致不同的代码转换。为了更好地理解它是如何工作的,我们将使用以下代码作为重构的输入:structImageFetcher{funcfetchImages(completion:@escaping(Result<[UIImage],Error>)->Void){//..ExecuteDataRequest}}ConvertFunctiontoAsync第一个重构选项将fetchImages方法转换为异步变量,留下非异步变量。如果您不想保留原始实现,此选项很有用。生成的代码如下:structImageFetcher{funcfetchImages()asyncthrows->[UIImage]{//..performdatarequest}}AddAsyncAlternative添加AsyncAlternative重构选项确保保留旧的实现,但是一个可用的属性将添加:structImageFetcher{@available(*,renamed:"fetchImages()")funcfetchImages(completion:@escaping(Result<[UIImage],Error>)->Void){Task{do{letresult=tryawaitfetchImages()completion(.success(result))}catch{completion(.failure(error))}}}funcfetchImages()asyncthrows->[UIImage]{//..执行数据请求}}可用的属性非常多有助于了解您需要在何处更新代码以适应新的并发变量。虽然,Xcode提供的默认实现没有任何警告,因为它没有标记为已弃用。为此,你需要调整可用的标记,像这样:@available(*,deprecated,renamed:"fetchImages()")使用这个重构选项的好处是它可以让你逐渐适应新的结构Concurrent更改而无需立即转换整个项目。在两者之间进行构建很有价值,这样您就知道您的代码更改正在按预期工作。使用旧方法的实现将收到以下警告。您可以在整个项目中逐步更改您的实现,并使用Xcode中提供的Fix按钮自动转换您的代码以利用新的实现。添加异步包装器(AddAsyncWrapper)最后的重构方法将使用最简单的转换,因为它只会利用你现有的代码:structImageFetcher{@available(*,renamed:"fetchImages()")funcfetchImages(completion:@escaping(Result<[UIImage],Error>)->Void){//..执行数据请求}funcfetchImages()asyncthrows->[UIImage]{returntryawaitwithCheckedThrowingContinuation{continuationinfetchImages(){resultincontinuation.resume(with:result)}}}}新添加的方法利用了Swift中引入的withCheckedThrowingContinuation方法,它可以毫不费力地转换基于闭包的方法。不抛出的方法可以使用withCheckedContinuation,效果一样,但不支持抛出错误。这两个方法暂停当前任务,直到调用给定的闭包以触发async-await方法的继续。换句话说:您必须确保针对您自己的基于闭包的方法的回调调用延续闭包。在我们的例子中,这归结为使用我们从原始fetchImages回调返回的结果值调用continue。为您的项目选择正确的异步等待重构方法这三个重构选项应该足以将您现有的代码转换为异步代码。根据项目的大小和重构时间,您可能希望选择不同的重构选项。但是,我强烈建议逐步应用更改,因为它允许您隔离更改的部分,使您更容易测试您的更改是否按预期工作。SolvingtheerrorSolvingthe“Referencetocapturedparameter'self'inconcurrently-executingcode”错误使用异步方法时的另一个常见错误如下:“Referencetocapturedparameter'self'inconcurrently-executingcode”大致意思是说我们试图引用一个不可变的self实例。换句话说,您可能指的是属性或不可变实例,例如,像本例中的结构:不支持从异步执行代码修改不可变属性或实例。可以通过使属性可变或将结构更改为类等引用类型来修复此错误。枚举的async-await结束会是Result枚举的结束吗?我们已经看到异步方法取代了使用闭包回调的异步方法。我们可以问问自己,这是否就是Swift中Resultenum[2]的结尾。最终我们会发现我们真的不再需要它们了,因为我们可以利用try-catch语句结合async-await。Result枚举不会很快消失,因为它仍在Swift项目的许多地方使用。但是,一旦async-await的采用率增加,我不会感到惊讶。就个人而言,除了完成回调之外,我不会在任何地方使用结果枚举。一旦我完全使用async-await,我就不会再使用这个枚举了。结论Swift中的async-await允许结构化并发,这应该会提高复杂异步代码的可读性。完成不再需要闭包,并且在彼此之后调用多个异步方法的可读性大大增强。可能会出现一些新类型的错误,这将通过确保从支持并发的函数调用异步方法而不更改任何不可变引用来解决。参考文献[1]ChrisLattner的SwiftConcurrencyManifesto:https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782[2]结果枚举:https://www.avanderlee.com/swift/result-enum-type/
