Swift 开发必备 tips 阅读笔记 三 最后一篇

Swift与开发环境及一些实践

Posted by poos on May 18, 2019

tips

全3篇,如下,这是第三篇。

Swift 开发必备 tips 阅读笔记 第一篇,Swift 语言的新特性。

Swift 开发必备 tips 阅读笔记 第二篇,Objective-C 与 Swift 的一些特性过渡。

Swift 开发必备 tips 阅读笔记 三 最后一篇,Swift与开发环境及一些实践。

Swift与开发环境及一些实践

Swift 命令行工具

Swift 的 REPL (Read-Eval-Print Loop) 环境可以让我们使用 Swift 进行简单的交互式编程。但是,REPL 环境只是表现得像是即时的解释执行,但是其实质还是每次输入代码后进行编译再运行。这就限制了我们不太可能在 REPL 环境中做很复杂的事情。

可以在 Xcode 中的 command line tools 设置 xcrun swift 来启动 REPL 环境。

也可以直接使用终端命令行使用 swift 命令执行。

还可以使用 swiftc 编译出y一个 main.swift,执行。也能编译出汇编代码分析。

随机数生成

随机数生成一直是程序员要面临的大问题之一,在高中电脑课堂上我们就知道,由 CPU 时钟,进程和线程所构建出的世界中,是没有真正的随机的。在给定一个随机种子后,使用某些神奇的算法我们可以得到一组伪随机的序列。

arc4random 是一个非常优秀的随机数算法,并且在 Swift 中也可以使用。它会返回给我们一个任意 整数,我们想要在某个范围里的数的话,可以做模运算 ( % ) 取余数就行了。但是有个陷阱..

arc4random 返回一个 64 位的值,所以在iphone 5 以前的机型会整形越界而崩溃。可以写一个函数,使用 Int46,然后进行封装,只返回 Int32.max 以内的随机数。

不使用上边的方式,而使用 Range 创建随机数,也是一个不错的选择。

实现对象的 description 方法可以方便的打印调试信息。

如果 description 方法遵循的是 CustomDebugStringConvertible 则只在 debug 调试模式打印信息。反正,如果使用 CustomStringConvertible 则总是可以使用。

错误和异常处理

在开始这一节的内容之前,我想先阐明两个在很多时候被混淆的概念,那就是异常(exception)和错误(error)。

在Objective-C开发中,异常往往是由程序员的错误导致的 app 无法继续运行:比如我们向一个无法响应某个消息的 NSobject 对象发送了这个消息,会得到 NSInvalidArgumentException 的异常,并告诉我们 “unrecognized selector sent to instance”;比如我们使用一个超过数组元素数量的下标来试图访问 NSArray 的元素时,会得到 NSRangeException 。类似由于这样所导致的程序无法运行的问题,应该在开发阶段就被全部解决,而不应当出现在实际的产品中。

相对来说,由 NSError 代表的错误更多地是指那些 “合理的”,在用户使用 app 中可能遇到的情况:比如登陆时用户名密码验证不匹配,或者试图从某个文件中读取数据生成NSData 对象时发生了问题(此如文件被意外修改了)等等。

但是 NSError 的使用方式其实变相在鼓励开发者忽略错误。

想一想在使用一个带有错误指针的 API 时我们做的事情吧。我们会在 API 调用中产生和传递 NSError, 并藉此判断调用是否失败。作为某个可能产生错误的方法的使用者,我们用传入NSErrorPointer 指针的方式来存储错误信息,然后在调用完毕后去读取内容,并确认是否发生了错误。

比如在Objective-C中,我们会写类似这样的代码:

NSError *error;
BOOL success = [data writeToFile: path options: options error: &error] ;
if(error) {
    //发生错误
}

这非常棒,但是有一个问题:在绝大多数情况下,这个方法并不会发生什么错误,而很多工程师也为了省事和简单,会将输入的 error 设为 nil, 也就是不关心错误(因为可能他们从没见过这个 API 返回错误,也不知要如何处理)。

于是调用就变成了这样:

[data writeToFile: path options: options error: nil];

但是事实上这个API调用是会出错的,比如设备的磁盘空间满了的时候,写入将会失败。但是当这个错误出现并让你的 app 陷入难堪境地的时候,你几乎无从下手进行调试–因为系统曾经尝试过通知你出现了错误,但是你却选择视而不见。

在 Swift2.0 中,Apple 为这内语言引入了异常机制。现在,这类带有 NSError 指针作为参数的 API都被改为了可以抛出异常的形式。比如上面的 writeToFile:options:error: ,在Swit中变成 了:

1
2
open func write(toFile path: String,
    options writeOptionsMask: NSData . WritingOptions) throws

我们在使用这个 API 的时候,不再像之前那样传入一个 error 指针去等待方法填充,而是变为使 用 try catch 语句:

1
2
3
4
5
6
do {
    try d.write(toFile: "hello", options: [])
    //try <#throwing expression#>
} catch <#pattern#> {
    <#statements#>
}

如果你不使用 try 的话,是无法调用 write(toFile:options:) 方法的,它会产生一个编译错误,这让我们无法有意无意地忽视掉这些错误。

更多的,可以使用 switch case 来处理不同错误的处理。在 Swift 2.0 以后,已经可以使用 enum 来枚举错误。

枚举还能解决 Optional 的一些问题。

断言

默认情况下只在 Release 的情况下断言才会被禁用,但是有时候我们可能出于某些目的希望断言在调试开发时也暂时停止工作,或者是在发布版本中也继续有效。我们可以通过显式地添加编译标记达到这个目的。在对应 target 的 Build Settings 中,我们在 Swift Compiler - Custom Flags 中的 Other Swift Flags 中添加 -assert-config Debug 来强制启用断言,或者 -assert-config Release 来强制禁用断言。当然,除非有充足的理由,否则并不建议做这样的改动。如果我们需要在 Release 发布时在无法继续时将程序强行终止的话,应该选择使用 fltalError 。

1
assert(<#T##condition: Bool##Bool#>, <#T##message: String##String#>)

fatalError

1
2
fatalError(<#T##message: String##String#>)

在调试时我们可以使用断言来排除类似这样的问题,但是断言只会在 Debug 环境中有效,而在 Release 编译中所有的断言都将被禁用。在遇到确实因为输入的错误无法使程序继续运行的时候,我们一般考虑以产生致命错误(fatalError) 的方式来终止程序。

使用上:一个最常见并且合理的需求就是“抽象类型或者抽象函数”。在很多语言中都有这样的特性:父类定义了某个方法,但是自己并不给出具体实现,而是要求继承它的子类去实现这个方法,而在 Objective-C 和 Swift 中都没有直接的这样的抽象函数语法支持,虽然 在 Cocoa 中对于这类需求我们有时候会转为依赖协议和委托的设计模式来变通地实现,但是其实Apple自己在Cocoa中也有很多类似抽象函数的设计。

比如 UIActivity 的子类必须要实现一大堆指定的方法,而正因为缺少抽象函数机制,这些方法都必须在文档中写明

在面对这种情况时,为了确保子类实现这些方法,而父类中的方法不被错误地调用,我们就可以利用 fatalError 来在父类中强制抛出错误,以保证使用这些代码的开发者留意到他们必须在自己的子类中实现相关方法:

在 Swift 2 推出的协议编程的概念(Protocol-Oriented Programming)的概念。通过使用协议,我们可以将需要实现的方法定义在协议中,遵守协议的类型必须实现这个方法。相比起“模拟的抽象函数”的方式,面向协议编程能够提供编译时的保证,而不需要将检查推迟到运行的时候。

1
2
3
4
5
class MyClass f
    func methodMustBeImplementedInSubclass() {
        fatalError("需要在子类重写")
    }
}

不仅仅是对于类似抽象函数的使用中可以选择 fatalError,对于其他一切我们不希望别人随意调用,但是又不得不去实现的方法,我们都应该使用fatalError 来避免任何可能的误会。

比如父类标明了某个init 方法是required 的,但是你的子类永远不会使用这个方法来初始化时,就可以采用类似的方式。

被广泛使用(以及被广泛讨厌的) init(coder: NSCoder) 就是一个例子。在子类中,我们往往会写:

1
2
3
required init(coder: NSCoder) {
    fatalError("NSCoding not supported" )
}

来避色编译错误.

代码组织和Framework

最初级和原始的静态库是以 .a 的二进制文件加上一些 .h 的头文件进 行定义的形式提供的,这样的静态库在使用时比较麻烦,我们除了将其添加到项目和配置链接 外,还需要进行指明头文件位置等工作。

而 Apple 自己的框架都是 .framework 为后缀的动态框架,是集成在操作系统中的,我们使用这些 框架的时候只需要在 target 配置时进行指明就可以,非常方便。 从 Xcode 6 开始 Apple 官方提供了单独制作类似的 framework 的方法,这种便利性可能会使代码 的组织方式发生重大变化。

现在可以方便的创建私有 .framework 库。而且很多组件化开发方案往往使用 .framework 解耦。

安全的资源组织方式

主要是手写图片名字的问题。

现在,Swift 社区中现在也有一些比较成熟的自动化工具了,R.swift 和 SwiftGen 就是其中的佼佼者。它们通过扫描我们的项目文件,来 提取出对应的字符串,然后自动生成对应的 enum 或者 struct 文件。当我们之后添加,删除或者改变资源名称的时候,这些工具可以为我们重新生成对应的代表资源名字的类型,从而让我们可以利用编译器的检查来确保代码中所有对该资源的引用都保持正确。

Playground 延时运行

Playground 默认单此运行即结束了,所以一些延时的 Timer 不能正常调试。例如调试网络请求超时的问题

通过添加下边的一行代码,可以实现最长等待 30s 的 Playground。

1
2
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

默认情况下它会在顶层代码最后一句运行后 30 秒的时候停止执行。

如果你想改变这个时间的话,可以通过 alt + cmd + 回车 来打开辅助编辑器。在这里你会看到控制台输出和时间轴,将右下角的 30 改成你想要的数 字,就可以对延时运行的最长时间进行设定了。

Playground与项目协作

Playground其实是可以用在项目里的,通过配置,我们是可以做到让Playground使用项目中已 有的代码的。直接说结论的话,我们需要满足以下的一些条件:

  1. Playground 必须加入到项目之中,单独的Playground是不能使用项目中的已有代码的; 最简单的方式是在项目中使用File -> New -> File….然后在里面选择Playground。注意不要直接选择File -> New -> Playgroun…否则的话你还需要将新建的Playground拖到项目中来。

  2. 想要使用的代码必须是通过Cocoa (Touch) Framework以一个单独的target的方式进行组织的;

  3. 编译结果的位置需要保持默认位置,即在Xcode设置中的Locations里Derived Data保持默认值;

  4. 如果是ios应用,这个框架必须已经针对iPhone 5s Simulator这样的64位的模拟器作为目标进行过编译;

ios 的 Playground 其实是运行在64位模拟器上的,因此为了能找到对应的符号和执行文件,框架代码的位置和编译架构都是有所要求的。

在满足这些条件后,你就可以在Playground中通过import 你的框架module名字来导入代码,然后进行使用了。

Playground可视化开发

import PlaygroundSupport

然后设置 Playground.current.liveVIew 即可。

数学和数字

Darwin 里的 math.h 定义了很多和数学相关的内容,它在 Swift 中也被进行了 module 映射,因此 在 Swift 中我们是可以直接使用的。有了这个保证,我们就不需要担心在进行数学计算的时候会和 标准有什么差距。

Swift 除了导入了 math.h 的内容以外,也在标准库中对极限情况的数字做了一些约定,比如我们 可以使用 .max 和 .mini 来取得对应平台某个类型 的最大和最小值。

另外在 Double 中,我们 还有两个很特殊的值, infinity(无限大) 和 NaN (Not a Number 未定义,或者错误运算)。

JSON

  • 关于解析 之前需要使用 NSJsonSerialization 解析。

从 swift 4.1 开始,swift 的 Foundation 库 即可直接解析:

1
2
3
4
5
6
7
//   let thought = try? newJSONDecoder().decode(Thought.self, from: jsonData)
import Foundation
// MARK: - Thought
struct Thought {
    let id: Int
    let content: String
}
  • 关于可选值

现在可以使用 if let 或者 guard 解析可选值。

NSNull

在 Objective-C 中,因为 NSDictionary 和 NSArray 只能存储对象,对于像 JSON 中可能存在的 null 值, NSDictionary 和 NSArray 中就只能用 NSNull 对象来表示。

在 Swift 中,这个问题被语言的特性彻底解决了。因为 Swift 所强调的就是类型安全,无论怎么说都需要一层转换。因此除非我们故意犯二不去将 AnyObject 转换为我们需要的类型,否则我们 绝对不会错误地向一个 NSNull 发送消息。

文档注释

1
2
3
4
/// class
/// 
/// this is ...

性能考虑

在 WWDC 14 的 Keynote 上,Swift 相比于其他语言的速度优势被特别进行了强调。

但是这种速度优势是有条件的,虽然由于编译器的进步我们可能可以在不了解语言特性的时候随便写也能得到性能上的改善,但是如果能够稍微理解背后的机制的话,我们就能投 “编译器所好”,写出更高 效的代码。

相比于 Objective-C,Swift 最大的改变就在于方法调用上的优化。在 Objective-C 中,所有的对于 NSObject 的方法调用在编译时会被转为 objc_msgSend 方法。这个方法运用 Objective-C 的运行时特性,使用派发的方式在运行时对方法进行查找。因为 Objective-C 的类型并不是编译时确定的, 我们在代码中所写的类型不过只是向编译器的一种“建议”,不论对于怎样的方法,这种查找的代价基本都是同样的。

Objective-C 运行时十分高效,相比于 I/O 这样的操作来说,单次的方法派发和查找并不会花费太多的时间,但实事求是地说,这确实也是 Objective-C 性能上可以改进的地方,这种改善在短时间内大量方法调用时会比较明显

Swift 因为使用了更安全和严格的类型,如果我们在编写代码中指明了某个实际的类型的话 (注意,需要的是实际具体的类型,而不是像 Any 这样的抽象的协议),我们就可以 向编译器保证在运行时该对象一定属于被声明的类型。这对编译器进行代码优化来说是非常有帮助的,因为有了更多更明确的类型信息,编译器就可以在类型中处理多态时建立虚函数表 (vtable),这是一个带有索引的保存了方法所在位置的数组。在方法调用时,与原来动态派发和查找方法不同,现在只需要通过索引就可以直接拿到方法并进行调用了,这是实实在在的性能提升

更进一步,在确定的情况下,编译器对 Swift 的优化甚至可以做到将某些方法调用优化为 inline 的 形式。比如在某个方法被 final 标记时,由于不存在被重写的可能,vtable 中该方法的实现就完全固定了。对于这样的方法,编译器在合适的情况下可以在生成代码的阶段就将方法内容提取到调用的地方,从而完全避免调用。

所以对于性能方面,我们应该注意的地方就很明显了。如果遇到性能敏感和关键的代码部分,我们最好避免使用 Objective-C 和 NSObject 的子类。在以前我们可能会选择使用混编一些 C 或者 C++ 代码来处理这些关键部分,而现在我们多了 Swift 这个选项。相比起 C 或者 C++,Swift 的语 言特性上要先进得多,而使用 Swift 类型和标准库进行编码和构建的难度,比起使用 C 或 C++ 来 要简单太多。另外,即使不是性能关键部分,我们也应该尽量考虑在没有必要时减少使用 NSObject 和它的子类。如果没有动态特性的需求的话,保持在 Swift 基本类型中会让我们得到更多的性能提升

Log输出

例如这样:

1
2
3
4
5
6
7
8
func printLog<T>(_ mes sage: T,
                file: String = #file,
                method: String = #function ,
                line: Int = #line)
    #if BEBUG           
        print("I((file as NSString).lastPathC omponent)[l(line)], (method): ) (message)")
    #endif
)

溢出

和其他一些编程语言的处理不同的是,Swift 在溢出的时候选择了让程序直接崩溃而不是截掉超出的部分,这也是一种安全性的表现。

另外,编译器其实已经足够智能,可以帮助我们在编译的时候就发现某些必然的错误。

1
2
var max = Int.max
max = max + 1 //编译器报错

最后,如果我们想要其他编程语言那样的对溢出处理温柔一些,不是让程序崩溃,而是简单地从高位截断的话,可以使用溢出处理的运算符,在 Swift 中,我们可以使用以下这五个带有 􏱚 的操 作符,这样 Swift 就会忽略掉溢出的错误:

  • 溢出加法 ( &+ )
  • 溢出减法 ( &- )
  • 溢出乘法 ( &* )
  • 溢出除法 ( &/ )
  • 溢出求模 ( &% )
1
2
3
4
var max = Int. max
max = max &+ 1
//64位
//max: -9223372036854775808

宏定义define

Swift 中没有宏定义了。

Swift 中将宏定义彻底从语言中拿掉了,并且 Apple 给了我们一些替代的建议:

  • 使用合适作用范围的 let 或者 get 属性来替代原来的宏定义值,例如很多 Darwin 中的 C 的 define 值就是这么做的:

var M_PI: Double { get }

  • 对于宏定义的方法,类似地在同样作用域写为 Swift 方法。例如 NSLocalizedString:
1
2
3
4
5
6
7
8
9
10
11
// objc
#define NSLocalizedString(key, comment) X
[[NSBundle mainBundle] localiz edS tringForKey:(key) value:@"" table:nil]

// Swift
func NSLocalizedString(
    key: String,
    tableName: String? = nil,
    bundle: NSBundle = Bundle.main ,
    value: String = "" ,
    comment: String) -> String
  • 随着 #define 的消失,像 #ifdef 这样通过宏定义是否存在来进行条件判断并决定某些代码是否参与编译的方式也消失了。但是我们仍然( -只能- )可以使用 #if 并配合编译的项目配置来完成条件编译,具体的方法可以参看条件编译一节的内容。

属性访问控制

  • private,作用域访问

  • fileprivate,文件访问

  • internal, 同一 module (或者说 target) 可用

  • public,可被别的框架访问,不能继承重写

  • open,可被别的框架访问,可以继承重写

Swift中的测试

Xcode 中集成了 XCTest 作为测试框架,Swift 代码的测试默认也使用这个框架进行。

XCTest 中测试和待测试的 app 是分别独立存在于两个不同的 target 里的。这在 Swift 2.0 之前使测试 Swift 代码时面临了由访问权限带来的巨大困境

在 Objective-C 时代,测试的 target 通过依 赖应用 target 并导入头文件来获取 app 的 API 并对其进行测试。而在 Swift 中因为 module 模块 的管理方法和访问控制权限的设计,使得这个过程出现了问题:一般对于 app,我们都不会将方 法标记为 public ,而会遵循访问权限最小的原则,使用默认的 internal 或者是 private 。对于 有些 internal 的方法,其实我们是需要去进行测试的。但是由于测试的 target 和 app 的 target 是不同的,因此在测试中导入 app 的 module 后我们是访问不到那些默认 internal 的待测试方法 的,这就使得测试变得不可能了。

如果我们正在开发的是一个类库的话,为了别人能导入和使用我们的库,我们需要把对外的方法 和成员都标记成 􏰤􏰊􏰓􏰇􏰅􏰆 ,这样我们就能直接在测试 target 中导入类库 target 并访问到待测试 API 了。

在 Swift 2.0 中,XCTest 使用了另外的方式。 Apple 为 app 的测试开了“后門”。现在我们可以通过在测试代码中导入 app 的 target 时,在之前追加 @testable ,就可以访问到 app target 中 internal 的内容了

@testable import MyApp

Core Data

相信大多数开发者第一次接触到 Objective-C 的 @dynamic 都是在和 Core Data 打交道的时候。 Objective-C 中的 @dynamic 和 Swift 中的 @dynamic 关键字完全是两回事。在 Objective-C 中,如果 我们将某个属性实现为 @dynamic 的话,就意味着告诉编译器我们不会在编译时就确定这个属性的 行为实现,因此不需要在编译期间对这个属性的 getter 或/及 setter 做检查和关心。这是我们向编 译器做出的庄严承诺,表示我们将在运行时来提供这个属性的存取方法 (当然相应地,如果在运行 时你没有履行这个承诺的话,应用就会挂给你看)。

很遗憾,Swift 里是没有 @dynamic 关键字的,因为 Swift 并不保证一切都走动态派发,因此从语言 层面上提供这种动态转发的语法也并没有太大意义。在 Swift 中严格来说是没有原来的 @dynamic 的完整的替代品的,但是如果我们将范围限定在 Core Data 的话就有所不同。

Core Data 是 Cocoa 的一个重要组成部分,也是非常依赖 @dynamic 特性的部分。Apple 在 Swift 中专門为 Core Data 加入了一个特殊的标注来处理动态代码,那就是 @NSManaged 。我们只需要在 @NSManagedObject 的子类的成员的字段上加上 @NSManaged 就可以了.

1
2
3
class MyModel: NSManagedObject {
    @NSManaged var title: String
}

这时编译器便不再会纠结于没有初始化方法实现 title 的初始化,而在运行时对于 MyModel 的读写也都将能利用数据图完成恰当的操作了。

最后要指出一点,Apple 在文档中指出 @NSManaged 是专内用来解决Core Data中动态代码的问题的,因此我们最好是遵守这个规则,只在NSManagedbject 的子类中使用它

但是如果你将 @NSManaged 写到其他的类中,也是能够编译通过的。在这种情况下,被标记的属性的访问将会回滚到 Objective-C 的 getter 和 setter 方法。也即,对于一个叫做title 的属性,在运行时会调用 title 和 setTitle: 方法。行为,上来说和以前的 @dynamic 关键字是一样的,我们当然也可以使用 Objective-C 运行时来提供这两个方法,但是要注意的是这么做的话我们就必须对涉及到的类和方法标记为 @objc 。我并不推荐这样做,因为你无法知道这样的代码在下一-个版本中是否还能工作。

闭包歧义

Swift 的闭包写法很多,但是为了防止闭包歧义,最正规的应该是完整地将闭包的输入和输出都写上,然后用 in 关键字隔离参数和实现。尤其是使用多个闭包返回值嵌套的时候。

1
2
3
4
5
{ (<#parameters#>) -> <#return type#> in
    <#statements#>
}

在早些版本1.2时候,可能发生歧义。为了增强可读性和安全性,最直接是在调用时尽量指明闭包参数的类型。

泛型扩展

Swift 对于泛型的支持使得我们可以避免为类似的功能多次书写重复的代码,这是一种很好的简化。而对于泛型类型,我们也可以使用 extension 为泛型类型添加新的方法。

与为普通的类型添加扩展不同的是,泛型类型在类型定义时就引入了类型标志,我们可以直接使用。例如 Swift 的 Array 类型的定义是:

1
2
3
4
5
6
7
8
public struct Array<Element> : CollectionType, Indexable, ... {
//...

    //在扩展中是不能添加整个类型可用的新泛型符号的,但是对于某个特定的方法来说,我们可以添加 T 以外的其他泛型符号。
    func appendRandom<U: Some>(_ input: U) -> String{
        return ...
    }
}

兼容性

基本上相邻几个版本是兼容的。

Xcode 也会提供一些迁移工具,帮助迁移到新的 swift 版本。

基本上除了1-2,2-3,3-4,后边的版本改动都没有很多,总得代码量有限的话,可以直接硬性升级,然后修改错误的语法即可。

列举 enum 类型

**enum 像 struct 或者 class 一样可以遵循协议,添加方法。

因为 swift 枚举是可以传入参数的,所以不能用 for … in 列举所有枚举

  • 带来的好处是,枚举可以实现类簇,switch case 相关的功能。例如著名的网络请求库 Maya 就是有了这个特性。

  • 但是我们可以实现 static 方法,返回一个数组集合。当然也可以封装协议。另一种可以考虑的方式是元编程的方式,自动生成相关代码。

尾递归

递归在程序设计中是一种很有用的方法,它可以将复杂的过程用易于理解的方式转化和描述。举个例子,比如我们想要写一个从 0 累加到 n 的函数,如果我们不知道等差数列求和公式的话,就 可以用递归的方式来做:

1
2
3
4
5
6
7
8
9
func sum(_ n: UInt) -> UInt {
    if n == 0 {
        return 0
    }
    return n + sum(n - 1)
}

sum(4) // 10
sum(100) // 5050

看起来没问题。但是我们如果用一个大一点的数的话,运行时就会出现错误,比如:

1
2
sum(100000)
// WXC_BAD_ACCESS (code = 2, address = ...)

这是因为每次对于 sum 的递归调用都需要在调用栈上保存当前状态,否则我们就无法计算最后的 n + sum(n - 1) 。当 n 足够大,调用栈足够深的时候,栈空间将被耗尽而导致错误,也就是我们常说的栈溢出了。

一般对于递归,解决栈溢出的一个好方法是采用尾递归的写法。顾名思义,尾递归就是让函数里的最后一个动作是一个函数调用的形式,这个调用的返回值将直接被当前函数返回,从而避免在栈上保存状态。这样一来程序就可以更新最后的栈帧,而不是新建一个,来避免栈溢出的发生。

1
2
3
4
5
6
7
8
9
10
11
func tailSum(_ n: UInt) -> UInt {
    func sumInternal(_ n: UInt, current: UInt) -> UInt {
        if n == 0 {
            return current
        } else {
            return sumInternal(n - 1, current: current + n)
        }
    }
    return sumInternal(n, current: 0)
}
tailSum(100000)

之所以删除了,是因为在最新的 swift5中,函数(指针)栈和常量栈其实没什么区别,调用次数过多时候一样会因为栈溢出而异常。实际开发中也需要注意一下,不要操作次数太多,占用超出栈了。