如何在 Swift 中进行错误处理

本文翻译自 Olivier Halligon 发布在AliSoftware的文章Let it throw,文章版权由 AliSoftware 授给SwiftGG翻译组。翻译者为博主JackAlan

今天的文章将会讲解关于如何在 Swift 中进行错误处理。

说实话,为了配合这个冬季❄️☃️,我取了一个有趣的文章标题。

译者注:原文标题为 Let it throw, Let it throw! 是模仿冰雪奇缘的主题曲 Let it go ,并且文章的副标题也在模仿冰雪奇缘的经典台词。

Objective-C 以及对应的 NSError

还记得 Objective-C 吗?那时1,官方的方法是通过传入一个 NSError* 的引用进行错误处理。

1
2
3
4
5
6
7
8
NSError* error;
BOOL ok = [string writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
if (!ok) {
NSLog(@“发生了一个错误: %@", error);
}

那简直是太痛苦了。以至于许多人不想甚至是懒得去检查错误,只是简单的在那里传一个 NULL 。这是很不负责不安全的行为。

抛出一个错误

随着 Swift 2.0 的到来,苹果决定采用一种不同的方式进行错误处理:使用 throw 2

使用 throw 非常的简单:

  • 如果你想创建一个可能出错的函数,用 throws 标记在它的签名处;
  • 如果需要的话,可以在函数中使用 throw someError
  • 在调用的地方,你必须明确的在能抛出错误3的方法的前面使用 try ;
  • 可以使用 do { … } catch { … } 这样的结构用来捕获并处理错误。

看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个可以抛错误的方法…
func someFunctionWhichCanFail(param: Int) throws -> String {
...
if (param > 0) {
return "somestring"
}
else {
throw NSError(domain: "MyDomain", code: 500, userInfo: nil)
}
}

// … 然后调用这个方法
do {
let result: String = try someFunctionWhichCanFail(-2)
print("success! \(result)")
}
catch {
print("Oops: \(error)")
}

错误再也阻挡不了我了

你可以看到 someFunctionWitchCanFail 返回了一个普通的 String ,当一切正常的情况下, String 也是其返回值的类型。先考虑最简单的情况(在 do { … } 中的),“通常情况下”可以很方便的调用这个函数去处理没有错误发生的情况。

唯一的这些方法可能会出错的提醒就是try关键字,编译器强制让你把 try 添加到方法调用的位置的前面,否则就像是调用一个无抛出错误的方法。然后,只需要在一个单独的地方(在 catch 里)写错误处理的代码。

要注意的是你可以在 do 代码段中写多于一行的代码(并且 try 可以调用不止一个抛错误的方法)。如果一切顺利的话,将会像预期的那样执行那些方法,但是一旦方法出错就会跳出 do 代码段进入声明 catch 处。对于那些有很多潜在错误的大段代码来说,你可以在一个单一的错误路径中处理所有的错误,这也是非常方便的。

NSError 有点挫了

OK,在这个例子下,我们仍然得用 NSError 处理错误,这有点痛苦。用 == 来比较域和错误代码,以及制作一个域和常量代码的列表,只是为了知道我们得到了什么错误以及如何正确的处理。。。哎哟。

但是我们可以解决这个问题!如果用Enums as Constants这篇文章里的知识:用 enum 替代 errors?

好吧,有一个好消息,那就是苹果提供了新的错误处理模式。事实上,当一个函数抛出错误时,它可以抛出任何遵从 ErrorType 的错误。 NSError 是其中的类型之一,但是你也可以自己搞一个,苹果也推荐这么做。

最适合 ErrorType 类型的就是 enum 了,如果有需要的话,甚至二者之间可以有关联值。比如:

1
2
3
4
5
6
enum KristoffError : ErrorType {
case ClumsyWayHeWalks
case GrumpyWayHeTalks
case PearShapedSquareShapedWeirdnessOfHisFeet
case NotWashedSince(days: Int)
}

然后你现在就可以在一个函数里使用 throw KristoffError.NotWashedSince(days: 3)来抛出错误,然后在调用的地方使用 catch KristoffError.NotWashedSince(let days)来处理这些错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func loveKristoff() throws -> Void {
guard daysSinceLastShower == 0 else {
throw KristoffError.NotWashedSince(days: daysSinceLastShower)
}
...
}

do {
try loveKristoff()
}
catch KristoffError.NotWashedSince(let days) {
print("Ewww, he hasn't had a shower since \(days) days!")
}
catch {
// Any other kind of error, whatever it is
print("I prefer we stay friends")
}

相比此前,这种方式更容易的捕获错误!

这也使得错误拥有清晰的名字、常量以及关联值。再也没有复杂的 userInfo 了,在 enum 中你可以清晰地看到值的关联,就像如上例子中的 days,并且它只对特定的类型有效(不会对 ClumsyWayHeWalks 中的 days 关联值有效)。

根本拿不回来

当你调用一个抛出错误函数,错误就会被函数中的 do...catch 捕获。但是如果错误没有被捕获,它就会被传递到上一层。比如:

1
2
3
4
5
6
7
8
9
10
11
12
func doFail() throws -> Void { throw … }

func test() {
do {
try doTheActualCall()
} catch {
print("Oops")
}
}
func doTheActualCall() throws {
try doFail()
}

这里,当 doFail 被调用时,潜在的错误没有被 doTheActualCall 捕获(没有 do...catch 捕获错误),所以它就被传递到 test() 函数。由于 doTheActualCall 没有捕获任何错误,所以它必须被标记为 throws :即使它不能通过自己抛出错误,但仍能传递。它自己不能处理错误,必须传递到更高层。

另一方面,test() 在内部捕获所有的错误,所以,即使它调用一个抛出函数(try doTheActualCall()),那个函数抛出的所有的错误也会在 do...catch 块中被捕获。函数 test() 本身不抛出错误,所以调用者也不要知道其内部行为。

隐藏,不要让他们知道

你现在可能想知道,如何知道方法到底抛出哪种错误。的确,被 throws 标记的函数到底能抛出哪种 ErrorType?它能抛出 KristoffErrors JSONErrors 或者其他类型的吗?我到底需要捕获哪种呢?

好吧,这的确是个问题。目前,由于二进制接口以及弹性问题4,这还是不可能的。唯一的方式就是你代码的文档。

但这仍然是一件好事。比如想象你用两个库,MyLibA中函数 funcA 会抛出 MyLibAError 错误,MyLibB中函数 funcB 会抛出 MyLibBError 错误。

然后你可能想创建你自己的库 MyLibC ,封装之前的两个库,用函数 funcC() 调用 MyLibA.funcA()MyLibB.funcB()。所以,函数 funcC 的结果可能会抛出 MyLibAError 或者 MyLibBError。而且,如果你添加了另一个抽象层,这就变得很糟糕了,会有更多的错误类型被抛出。如果我不得不把它们都列出来,并且调用的地方需要把它们全部捕获,这将会造成一堆冗长的签名和 catch 代码。

别让他们进来,别让他们看见

基于上面的原因,也为了防止你的内部错误超出你的库的作用域,以及为了限制那些必须由用户处理的错误类型的数量,我建议你把错误类型的作用域限制到每个抽象层次。

在如上的例子中,你应该抛出 MyLibCErrors 取而代之,而不是让 funcC 直接传递 MyLibAErrorsMyLibBErrors。我的建议有如下的两个原因,都是和抽象相关的:

  1. 你的用户不应该需要知道你在内部使用哪个库。如果将来的某天,你决定改变你的实现:使用 SomeOtherPopularLibA 替代MyLibA,显然这个库不会抛出相同的错误,你自己的 MyLibC 框架的调用者不需要知道或关心。这就是抽象的全部。
  2. 调用者不应该需要处理所有的错误。当然你可以捕获那些错误中的一些并且在内部处理:把 MyLibA 抛出的所有错误都暴露给用户是没有意义的,比如一个 FrameworkConfigurationError 错误表明你误用了 MyLibA 框架并且忘了调用它的 setup() 方法,或者是任何不应该由用户做的事情,因为用户根本无能为力。这种错误是你的错误,而不是别人的。

所以,取而代之,你的 funcC 应该很可能捕获所有 MyLibAErrorsMyLibBErrors,封装它们为 MyLibCErrors 替代。这样的话,你的框架的使用者不需要知道你在内部使用了什么。你可以在任何时候改变你的内部实现和使用的库,并且你只需要给用户暴露那些他们需要关注的错误。

其他资料分享 5

译者注:原标题为 We finish each others sandwiches,是在模仿冰雪奇缘中王子和公主的对话,表示和其他博主以及读者的一种亲近的关系。

本来还有一些东西要讲,关于 throw 和 Swift 2.0 的错误处理模型,我本可以讲一些关于 try?try!,关于高阶函数中的 rethrows 关键字。

这里,没有时间面面俱到了,那会使得我的文章非常长。但是别的有趣的文章可能会帮你探索 Swift 错误处理的世界,包括(但不限于):


  1. 更多关于在 Objective-C 中错误处理的信息,可以参考这篇文章:NSError。今天的文章是关于 Swift 中的新方式的,所以别在旧事物上花费太多的时间。
  2. 尽管它叫 throw ,但是 throw 不是像 Java 或者 C++ 甚至 OC 中的 throw exception。但是使用的方式非常相似,苹果决定保留相同的措辞,所以习惯于 exceptions 的人会感到非常自然。
  3. 这是编译器强制的,其目的是让你意识到这个函数可能出错,你必须处理潜在的错误。
  4. Swift 2.0 还不支持 typed throws,但是这里有一个关于添加这个特性的讨论,这里 Chris Lattner 解释了 Swift 2 不支持的原因,以及为什么我们需要 Swift 3.0 的弹性模型以获得这个特性。
  5. 好了,我保证这是我最后一次可耻使用 Frozen 标题了。

合理的使用 Swift 中的 final 类

你上一次在 Swift 中继承一个类是什么时候?而且这个类是你创建的但不是 Cocoa 体系中的一部分。在 protocol 扩展和一般的 extension 扩展存在的情况下,你多久继承一次非 Cocoa 类型的 class ?

如果你的答案在 0% 和 5% 之间,你是相当具有代表性的。在 Swift 的类型体系中,引用类型不再使用继承来紧密耦合了。

进一步来说,你多久创建一次类和子类带着这样的意图:他们将来会被继承,通过一个特定模块之外的 API 客户端。(假设,当然,你不是 Apple,并且你没有在写 view 和 controller)。

当继承成为例外而不是规则,是时候考虑默认创建 final 类?或者将模块封装为 internal-by-default 更好?这样公开类在原模块外就不能被继承。

辩论目前如火如荼的展开在 Swift 进度列表中,关于这如何进行,以及是否应该这么做,是否类应该被设计为强制调用父类以重写方法(需要上层调用)。

John McCall 写道:


我们目前的意图是,公开的继承和重写将被锁定为默认,但是内部的继承和重写不会。我认为这达到了平衡,此外,这与一般语言的代码演进是一致的,通过以下几种方式将促进 “无副作用” 迅速发展:

(1) 避免手工管理的障碍,当你正在 hack 一个模块最初的实现代码,但是
(2) 不会让最初的实现代码在内部隐源,并且二进制兼容允许模块之外的代码,以及
(3) 提供良好的语言工具,来逐渐的把那些最初的原型接口构建为更强的内部抽象。

所有默认行为的硬限制都维系在模块边界上,因为我们假定,当你之前犯了一个错误的决定,这能直接的在模块内部修复任何问题。

所以,okay,默认一个类是可继承的,并且不是真的这样设计的,现在模块中有一些子类,这导致了一些问题。只要没人改变缺省情况 (他们本可以无所顾忌的在任何情况下做,但如果只需要一个外部的子类,那就不太可能去做),所有这些子类仍是模块内的,你仍然有自由的控制权,以纠正最初的错误设计。


Joe Groff 的想法:


强大的子类化能力要求有意识的设计,就像 API 设计的其他所有方面一样。


Jordan Rose:


关于此有趣的事情是,遗漏的错误:没有考虑是否一个类应该是 final ,这比另一种选择更糟糕。这里忽略一下优化,一个开始是 final 的类,之后就无法更改;这无法改变这个类目前是如何使用的,对于许多库发展问题,这是最好的答案: 缺省情况应该是安全的,类的设计者可以选择以后更进取。


HEXO 自动部署

用 hexo 有一阵子了,以前一直都是在本机做 generationdeploy,看到 github 有 webhooks 这样方便的API,所以就来给 HEXO 加个自动部署吧!

自动部署是非常酷的,如果你不想在本机做冗长的 generationdeploy 的话。自动部署的情况下只需要在客户端向仓库传递源码即可,剩下的都由 server 来自动完成。

hexo 默认会带一个 gitignore,所以用 git 保存 hexo 的源码非常的方便。

所以我的部署方案是:源码放在 github 私有仓,站点挂在公开仓库的 gh-pages 分支上。

需要 Server端与客户端 配置好 git、nodejs、hexo-cli 即可。 (一定要尽量保持版本一致性)

如下是客户端部署的脚本:

1
2
3
4
5
6
7
8
9
#!/bin/bash

echo -e "\033[32m [AUTO DEPLOY] git commit... \033[0m"
d=`date +%x-%T`
git add .
git commit -m "auto deploy at "${d}
echo -e "\033[32m [AUTO DEPLOY] git push... \033[0m"
git push origin master
echo -e "\033[32m [AUTO DEPLOY] git push finish! \033[0m"

记得加上执行权限 chmod +x deploy.sh.

如下是 server端部署的脚本:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

echo -e "\033[32m [AUTO SYNC] sync hexo start \033[0m"
cd /root/Blog/
echo -e "\033[32m [AUTO SYNC] git pull... \033[0m"
git pull origin master
echo -e "\033[32m [AUTO DEPLOY] hexo generate... \033[0m"
hexo g
echo -e "\033[32m [AUTO DEPLOY] hexo deploy... \033[0m"
hexo d
echo -e "\033[32m [AUTO DEPLOY] deploy hexo finish \033[0m"

同样,加上执行权限 chmod +x deploy.sh.

然后用 nodejs 搭一个简易的 server 用来接收 github 发来的 push 通知,这里利用的是github-webhook-handler,具体可以读它的文档,如下即是文档中的示例稍加改动后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var http = require('http')
var exec = require('child_process').exec;
var createHandler = require('github-webhook-handler')
var handler = createHandler({ path: '/webhook', secret: '********' });

http.createServer(function (req, res) {
handler(req, res, function (err) {
res.statusCode = 404;
res.end('no such location');
});
}).listen(7777);

handler.on('error', function (err) {
console.error('Error:', err.message);
});

handler.on('push', function (event) {
console.log('Received a push event for %s to %s',
event.payload.repository.name,
event.payload.ref);
exec('/root/Blog/sync.sh', function(err, stdout, stderr){
if(err) {
console.log('sync server err: ' + stderr);
} else {
console.log(stdout);
}
});
});

当然,这里可以启用forever来让这段代码在后台一直执行。

1
2
npm install -g forever
forever start server.js

当写完内容,想发布的时候只要执行./deploy.sh即可。

That`s it!

Update:

当你有多个域名时,可以弄一个自动 pull 的 webhook 即可,服务端可以直接用 Apache.

比如:iJack.pw JackAlan.com JackyGao.com 这三个域名,第一个是搭在 github 仓库的 gh-pages 上的,后面两个部署在服务器 Apache 上。

参考

ShangHai


在上海逛了三天,感谢SwiftGG的大家,稳重的PPT、古怪精灵的Cee、萌萌哒蛋蛋、胖胖的帮主、不正经的小青、万人迷的Sai 还有星星老师、小狗老师 ……

非常有幸能认识你们,大家在一起聊天、聚餐真的很开心。

KVO的缺陷


最近用了一些KVO,发现KVO本身有不少缺点,如下列举一二:

糟糕的API

KVO只有一个方法,也就是说很多时候不得不写一大坨代码来处理监听。如下是一个示例,代码如下:

1
2
3
4
5
6
7
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == _view && [keyPath isEqualToString:NSStringFromSelector(@selector(property))]) { // 确保KVO只响应需要的对象的属性
...
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; //父类也有可能需要监听
}
}

父类和子类中同时存在KVO,有坑

父类和子类监听了同一个对象的同一个参数时,很容易出现父类在dealloc中remove了一次,子类又remove了一次的情况下。

依赖KVC,这种硬编码的方式,编译期无法检查错误

如上的例子为了防止这一点使用了NSStringFromSelector来防止非法的属性,但是这只适用于单层的keyPath。

比如想监听一个vc的持有的scrollView的属性就不能这么做了。

参考:

Key-Value Observing Done Right

浅谈iOS中的闭包(三) - ARC 对 Block 做了什么?


在 OC 中,Block 有如下三种类型:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

如下是具体的描述

  • _NSConcreteGlobalBlock 是全局的静态block,不会访问任何外部变量。这种不捕捉外界变量的block是不需要内存管理的,这种block不存在于Heap或是Stack而是作为代码片段存在,类似于C函数。

  • _NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。

  • _NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。

在 MRC 下 block 在创建时是 stack 对象,如果我们需要在离开当前函数仍能够使用我们创建的 block 。我们就需要把它拷贝到堆上以便进行以引用计数为基础的内存管理。

例子

Global

1
2
3
4
5
6
- (void) viewDidLoad {
NSLog(@"%@",^{
int temp = 10;
printf("%d\n",temp);
});
}

Stack

1
2
3
4
5
6
- (void) viewDidLoad {
int temp = 10;
NSLog(@"%@",^{
printf("%d\n",temp);
});
}

Heap

1
2
3
4
5
6
- (void) viewDidLoad {
int temp = 10;
NSLog(@"%@",[^{
printf("%d\n",temp);
} copy]);
}

ARC做了什么?

文档里是这么说的:

With the exception of retains done as part of initializing a strong parameter variable or reading a weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

即在 ARC 下创建的 block 仍然是 _NSConcreteStackBlock 类型,当 block 被引用或返回时,ARC 帮助我们完成了 copy 和内存管理的工作。这种 block 变成了 _NSConcreteMallocBlock 类型。

浅谈iOS中的内存管理(五) - ARC中的Trick


以前一直都以为 ARC 只是做了在编译期帮助开发者寻找合适的位置插入诸如 release autorelease这样的事情,不过看了一些资料以后,发现 ARC 似乎并不仅仅在编译期有优化,甚至在运行时期也有一些优化,优化的目的之一就是巧妙地跳过调用autorelease方法。这里做一下总结。

正如前面几篇文章提到的那样,MRC 时代,如果一个方法创建了对象,然后 return 这个对象,那就需要手动调用对象的 autorelease 方法,以实现 非自己生成的对象自己也能持有 这样的需求。以前一直都以为编译器无非是在编译期帮我们找到了合适的位置插入 autorelease,但是现在看来,并不是这么简单,ARC 似乎更聪明一些。

ARC 中有一个非常巧妙地运行时的优化方案用来跳过某些情况下 autorelease机制的调用,如下是大致的流程:

当方法全部基于 ARC 实现时,在方法 return 的时候,ARC 会调用 objc_autoreleaseReturnValue() 替代 MRC 下的 autorelease。在 MRC 下想要 retain 的时候,ARC 会调用 objc_retainAutoreleasedReturnValue()

在调用 objc_autoreleaseReturnValue()时,会在栈上查询 return address 以确定 return value 是否会被传给 objc_retainAutoreleasedReturnValue。如果没传,说明返回值不能直接从提供方发送给接受方,这时就会调用 autorelease。反之,如果返回值能顺利地从提供方传送给接收方,那么就会跳过 autorelease 并修改 return address 以跳过 retain 的过程。

Code Zen


我想,代码的精神所在,大概就是把 immutable/duplicate 的东西封装,把 mutable 的东西解耦。

封装的东西可以是 class 可以是 model 也可以是 Block 这样的代码段。

怎么才能寻找到被封装的内容?索引咯。

对于字典,索引是 Key ;对于数组,索引是下标;对于更广泛的内容,索引是指针。

或许可以这样说,比如OC中:用类和对象封装父类指针和方法列表,用映射来找到方法的代码片段。

浅谈iOS中的闭包(二) - Copy Or Not?


昨天突然被提及到了一个问题:Block的作为属性,应该是copy 还是 strong?(当然,原问题不是这个,出于某些原因吧。)

很多人第一反应都是 copy 但是,我似乎在苹果的Tips上看到过,似乎strong也可以,所以只描述了一下堆栈拷贝流程,没敢确切的回答,回来翻了一下文档,果然,在 ARC 下都是可以的。

ARC 下,之所以 Block 还使用 copy 是从 MRC 遗留下来的,在 MRC 中,方法内部的 Block 实在栈区的,使用 copy 可以把它放到堆区。其实在 ARC 下写不写都OK。因为编译器自动对 Block 进行了 copy 操作。(Swift 中变成了 逃逸性闭包与非逃逸性闭包,可以参考我以前的文章。) 我猜也许好多人还坚持要在 ARC 下写 copy 可能是在时刻提醒自己吧。这样就会不犯二去手动拷贝属性值。

property 中 的NSString, NSArray, NSDictionary 使用 copy 比较常见,因为他们有对应的 mutable,为了确保这些不可变对象不会乱改变,所以应该在设置新属性的时候 copy 一份。

浅谈iOS中的内存管理(四) - Swift中的ARC


昨晚跟张瑞鹏谈起了Rust语言,其实一直对Rust语言充满了敬畏。Google了一下Rust vs Swift似乎也没什么靠谱的回答,可能二者目前区别不是很大。不过Quora上的一个回答引发了我的兴趣。

这里撇开Swift中的即时编译、交互式编程,只关注内存管理这方面。(Chris Lattner在他的博客中也提到这两者特性只是他个人的一时兴起。)

Troy Harvey说Rust的所有权系统(Ownership system)可以100%的防止内存泄露的发生,而Swift中使用的自动引用计数 98%可以杜绝内存泄露的发生(由于存在循环强引用)。

这里就来探索Swift中的ARC与此前OC中的ARC发生了哪些改变?这里把重点放在如何在Swift中解决循环强引用。

无主引用 (unowned reference)

在之前的三篇文章中,可以得知,在OC中weak可以用来破除两个强引用的循环引用。在Swift中,则又多了一个无主引用(unowned reference)。

弱引用(weak reference)和无主引用(unowned reference)都允许循环引用中的某一个实例引用另外一个实例而不保持强引用,这样实例就能够互相引用而不会产生循环强引用的副作用。

那么弱引用(weak reference)和无主引用(unowned reference)在Swift中又有什么区别呢?一般来说,对于在生命周期中会变为nil的实例使用弱引用(weak reference)。对于在初始化赋值后再也不会变为nil的实例,使用无主引用(unowned reference)。

无主引用永远都是有值的,这与弱引用不同。因此,无主引用总是被定义为非可选类型。可以在声明前加入unowned关键字以表示一个无主引用。

由于无主引用是非可选类型,所以不需要在使用时将其展开。无主引用总是可以被直接访问。不过ARC无法在实例被销毁后将无主引用设为nil,因为非可选类型不允许被赋值为nil

文档中的一个例子如下:

如下定义了CustomerCreditCard两个类,用于表示银行的客户和客户的信用卡。这两个类中,每一个都将另一个类的实例作为自身的属性。在这个数据模型中,有一点与其他循环强引用的例子不同:一个客户可能有或者没有信用卡,但是一张信用卡总是对应着一个确定的客户。为了表示这种关系,Customer类有一个可选类型的card属性,但是CreditCard类有一个非可选类型的customer属性。

由于信用卡总是对应着一个客户,因此将customer属性定义为无主引用,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Customer {
let name: String
var card: CreditCard?
init (name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized.")
}
}

class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit{
print("Card #\(number) is being deinitialized.")
}
}

在这个模型中,Customer实例持有对CreditCard实例的强引用, 而CreditCard实例持有对Customer实例的无主引用。

还有可能存在另一种场景:在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。

如下定义了CountryCity两个类,在这个模型中,每个城市必须属于一个国家,每个国家必须有一个首都城市。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String){
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}

class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}

Country的构造函数调用了City的构造函数。然而只有Country的实例完全初始化完后, Country的构造函
数才能把self传给City的构造函数。为了满足这种需求,通过在类型结尾处加上感叹号(City!)的方式,将CountrycapitalCity属性声明为隐式解析可选类型的属性。这表示像其他可选类型一样,capitalCity属性的默认值为nil,但是不需要展开它的值就能访问它。

由于capitalCity默认值为nil,一旦Country的实例在构造函数中给name属性赋值后,整个初始化过程就完成了。这代表一旦name属性被赋值后,Country的构造函数就能引用并传递隐式的selfCountry的构造函数在赋值capitalCity时,就能将self作为参数传递给City的构造函数。

以上的意义在于你可以通过一条语句同时创建CountryCity的实例,而不产生循环强引用,并且capitalCity的属性能被直接访问,而不需要通过感叹号来展开它的可选值。

闭包引起的循环引用

当一个闭包被赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时,也会出现循环引用。

如下的例子用来展示一个由闭包引起的循环引用的发生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name:String, text:String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized.")
}
}

这个例子中闭包在其闭包体内使用了self(引用了self.nameself.tex t),因此闭包捕获了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。

Note: 虽然闭包多次使用了self,但它只捕获HTMLElement实例的一个强引用。

Swift 中对此提供了一中优雅的解决方案叫做闭包捕获列表(closuer capture list)。

定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。

捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,可以声明每个捕
获的引用为弱引用或无主引用。

Note: Swift 有如下要求:只要是在闭包内使用的成员,就要用self.someProperty或者self.someMethod() (而不只是somePropertysomeMethod)。这提醒你可能会一不小心就捕获了self

于是,我们可以改造此前的asHTML闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}

init(name:String, text:String? = nil) {
self.name = name
self.text = text
}

deinit {
print("\(name) is being deinitialized.")
}
}

捕获列表常见的语法如下:

1
2
3
4
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}

如果闭包没有指明参数列表或者返回类型,则会通过上下文推断,那么可以把捕获列表和关键字in放在闭包最开始的地方:

1
2
3
4
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}