3.Swift 与开发环境及一些实践
随机数生成
随机数生成一直是程序员要面临的大问题之一,由 CPU 时钟、进程、线程所构建出的时间中,没有真正的随机的。在给定一个随机种子后,使用一些神奇的算法可以得到一组伪随机的序列。
arc4random 是一个非常优秀的随机数算法,
1 | let diceFaceCount = 6 |
Swift 的 Int 和 CPU 架构有关:32位的 CPU 实际是 Int32,64位 CPU 上是 Int64。arc4random 返回的值不论在什么平台上都是一个 UInt32, 32位平台上就有一半几率进行 Int 转换时越界。
这种情况使用一个 arc4random 的改良版本:
1 | func arc4random_uniform(_: UInt32) -> UInt32 |
最佳实践当然是为创建一个 range 的随机数的方法,这样我们就能在之后很容易的复用,甚至涉及类似 randomable 这样的接口了:
1 | func randomInRange(range: Range<Int>) -> Int { |
print 和 debugPrint
使用 extension 的方式实现为数众多的接口和各种各样的功能。一个普通的例子:
1 | class MyClass { |
调用 print 对其进行打印,只能打印出其类型。
对于 struct 来说,情况会好一些,打印一个 struct 实例的话,会列举出它所有成员的名字和值:
1 | struct Meeting { |
直接这样进行输出对了解对象的信息很有帮助,但存在很麻烦的问题。如果实例很复杂,我们很难再其中找到想要的结果,其次,对于 class 的对象来说,只能得到类型名字,毫无帮助。
正确的做法是使用 CustomStringConvertible 接口,这个接口定义了将该类型实例输出时所用的字符串。相对于直接在原来的类型定义中进行更改,
1 | extension Meeting: CustomStringConvertible { |
CustomDebugStringConvertible 与 CustomStringConvertible 的作用很类似,但是仅发生在调试中使用 debugger 来进行打印的时候输出。对于实现了 CustomDebugStringConvertible 接口的类型,我们可以在给 meeting 赋值后设置断点并在控制台使用类似 po meeting 的命令进行打印,控制台输出将为 CustomDebugStringConvertible 中定义的 debugDescription 返回的字符串。
错误和异常处理
exception error
exception 是由程序员的错误导致 app 无法继续运行,比如我们向一个无法响应某个消息的 NSObject 对象发送了这个消息,得到 NSInvalidArgumentException 的异常,比如我们使用一个超过数组元素数量的下标试图访问 NSArray 的元素时,会得到 NSRangeException 。
Error 代表的错误更多地指那些“合理的”,在用户使用 app 中可能遇到的情况,比如用户登录时密码校验不正确。
1 | enum LoginError: ErrorType { |
在 Swift 中,虽然把这块内容叫做 异常,但是实质上它更多的还是 “错误” 而非真正意义上的异常。
Swift 中的 try catch 和 Java 等不同,我们会把可能抛出异常的代码放在 do 中,并只在可能发生异常的语句前添加 try。
但 Swift 的异常机制也不是十全十美的。最大的问题是类型安全,不借助文档,我们是无法从代码中直接得知所抛出的异常的类型。比如上面的 login 方法,光看方法定义我们并不知道 loginError 会被抛出。
一个理想中的异常 API 应该是这样的:
1 | func login(user: String, password: String) throws LoginError |
另一个限制是对于非同步 API,抛出异常是不可用的,异常只是一个同步方法专用的处理机制。Cocoa 框架里对于异步 API 出错时,保留了原来的 NSError 机制。
1 | func dataTaskWithURL(_ url: NSURL, |
在日常开发中,往往不会直接调用这样的 API,会选择进行一些封装,以求更方便的调用和维护。一种比较常用的方式就是借助于 enum,作为 Swift 的一个重要特性,enum 类型现在可以与其他实例进行绑定,我们还可以让方法返回枚举类型:
1 | enum Result { |
断言
assertion 在 Cocoa 开发里一般用来在检查输入参数是否满足一定条件,并对其进行 论断。
使用时,举一个温度转换的例子:
我们想要把摄氏温度转换为开尔文温度的时候,因为绝对零度永远不能达到,所以我们不能接受一个小于-273.15摄氏度的温度作为输入:
1 | func convertToKelvin(# celsius: Double) -> Double { |
断言的另一个优点是它是一个开发时特性,只在 Debug 编译的时候有效,在运行时不被编译执行,因此断言不会消耗运行时性能。
虽然默认情况下只在 Release 的情况下断言才会被禁用,但是有时候我们可能出于某些目的希望断言在调试开发时也暂停工作,或者是在发布版本中也继续有效。我们可以通过显式地添加编译标记达到这个目的。
fatalError 用于 Release 发布时无法继续时将程序强行终止。
原来在 OC 中使用的断言函数 NSAssert 在 Swift 中已经被彻底移除。
fatalError
1 | enum MyEnum { |
default 没有返回任何东西,但是上述代码也能编译通过。
父类希望子类必须实现的方法可以在父类中写入 fatalError 抛出错误,来强制子类重写
1 | class MyClass { |
代码组织和 Framework
Apple 为了 iOS 平台的安全性考虑,是不允许动态链接非系统的框架的,因此在 app 开发中我们所使用的第三方框架如果是以库文件的方式提供的话,移动都是需要链接并打包最后的二进制可执行文件中的静态库。最初级和原始的静态库是以 .a 的二进制文件加上一些 .h 的头文件进行定义的形式提供的。
宏定义
Swift 将宏定义彻底从语言中拿掉了,并且 Apple 给了我们一些替代的建议:
1 | 使用合适范围内的 let 或者 get 属性来替代原来的宏定义值 |
尾递归
将函数里最后一个动作是一个函数调用的形式。这个调用的返回值将直接被当前函数返回。从而避免在栈上保存状态
1 | func tailSum(n: UInt) -> UInt { |