第8天 默认值、抛出异常
今天你将学习如何处理函数中的错误。这听起来可能有些消极,但就像约翰・列侬说的:“生活就是当你忙于制定其他计划时发生的事情”—— 没人愿意遇到问题,但生活总会不经意间出现各种问题!
幸运的是,Swift 让错误处理变得简单直观且相对可靠:它要求我们处理错误,或者至少承认错误可能会发生。如果你不至少尝试妥善处理错误,你的代码根本无法编译。
今天你需要完成两个教程,在这些教程中,你将学习参数的默认值和抛出函数,然后我们会总结函数的相关知识并完成 checkpoint 4。完成每个视频后,如果你想了解更多细节,有一段可选的额外阅读内容,还有一个简短的测试来帮助你确认是否理解了所教授的内容。
8.1 如何为参数提供默认值
作者:Paul Hudson 2021 年 10 月 7 日 已针对 Xcode 16.4 更新
向函数添加参数使我们能够增加自定义点,这样函数就可以根据我们的需求对不同的数据进行操作。有时我们希望保留这些自定义点以保持代码的灵活性,但其他时候你可能不想考虑这些 —— 你十有八九都想要同样的结果。
例如,之前我们看过这个函数:
func printTimesTables(for number: Int, end: Int) {
for i in 1...end {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(for: 5, end: 20)它会打印任何乘法表,从 1 乘以该数字开始,一直到任何终点。这个数字总是会根据我们想要的乘法表而变化,但终点似乎是提供合理默认值的好地方 —— 我们大多数时候可能希望数到 10 或 12,同时仍然保留在某些时候使用不同值的可能性。
为了解决这个问题,Swift 允许我们为任意数量的参数指定默认值。在这种情况下,我们可以将end的默认值设置为 12,这意味着如果我们不指定它,就会自动使用 12。
代码如下所示:
func printTimesTables(for number: Int, end: Int = 12) {
for i in 1...end {
print("\(i) x \(number) is \(i * number)")
}
}
printTimesTables(for: 5, end: 20)
printTimesTables(for: 8)注意,我们现在可以用两种不同的方式调用printTimesTables():我们可以在需要的时候提供两个参数,但如果我们不提供 —— 如果我们只写printTimesTables(for: 8)—— 那么end就会使用默认值 12。
实际上,我们之前使用过的一些代码中就有默认参数的例子:
var characters = ["Lana", "Pam", "Ray", "Sterling"]
print(characters.count)
characters.removeAll()
print(characters.count)这段代码向数组中添加一些字符串,打印其计数,然后删除所有字符串并再次打印计数。
作为一种性能优化,Swift 会给数组分配刚好足够的内存来容纳其元素,再加上一点点额外的内存,以便它们可以有一定的增长空间。如果向数组中添加更多元素,Swift 会自动分配更多的内存,这样可以尽可能减少浪费。
当我们调用removeAll()时,Swift 会自动删除数组中的所有元素,然后释放分配给数组的所有内存。这通常是你想要的,毕竟你删除这些对象是有原因的。但有时候 —— 只是有时候 —— 你可能马上要向数组中添加很多新元素,所以这个函数还有第二种形式,它在删除元素的同时保留之前的容量:
characters.removeAll(keepingCapacity: true)这是通过使用默认参数值来实现的:keepingCapacity是一个布尔值,默认值为 false,所以它在默认情况下会做合理的事情,同时也让我们可以在需要保留数组现有容量的时候传入 true。
如你所见,默认参数值让我们能够在保持函数灵活性的同时,不会让大多数时候的调用变得麻烦 —— 你只需要在需要一些特殊情况时传入某些参数。
【可选阅读】何时为函数使用默认参数
作者:Paul Hudson 2024 年 4 月 16 日 已针对 Xcode 16.4 更新
默认参数让我们可以为参数提供常见的默认值,从而使函数更易于调用。因此,当我们想要使用这些默认值调用函数时,我们可以完全忽略这些参数 —— 就好像它们不存在一样 —— 而我们的函数会正常工作。当然,当我们想要一些自定义的东西时,这些参数也可以供我们修改。
Swift 开发者经常使用默认参数,因为它们让我们可以专注于那些确实需要经常更改的重要部分。这确实有助于简化复杂的函数,并使你的代码更易于编写。
例如,想象一些路线查找代码如下:
func findDirections(from: String, to: String, route: String = "fastest", avoidHighways: Bool = false) {
// 代码在这里
}它假设大多数时候人们希望通过最快的路线在两个地点之间驾车,并且不避开高速公路 —— 这些合理的默认值可能在大多数情况下都适用,同时也让我们能够在需要时提供自定义值。
因此,我们可以用以下三种方式中的任何一种调用同一个函数:
findDirections(from: "London", to: "Glasgow")
findDirections(from: "London", to: "Glasgow", route: "scenic")
findDirections(from: "London", to: "Glasgow", route: "scenic", avoidHighways: true)大多数时候代码更简短、更简单,但在需要时又有灵活性 —— 非常完美。
【练习题】默认参数
问题 1/12:这段代码是有效的 Swift 吗 —— 是或否?
func calculateWages(payBand: Int, isOvertime: Bool = false) -> Int {
var pay = 10_000 * payBand
if isOvertime {
pay *= 2
}
return pay
}
calculateWages(payBand: 5, isOvertime: true)问题 2/12:这段代码是有效的 Swift 吗 —— 是或否?
func playGame(name: String, cheat: Bool = false) {
if cheat {
print("Let's play \(name); I bet I win!")
} else {
print("Let's play \(name)!")
}
}问题 3/12:这段代码是有效的 Swift 吗 —— 是或否?
func packLunchbox(number: Int, healthy: Bool = true) {
for _ in 0..<number {
if healthy {
print("I'm packing a healthy lunchbox.")
} else {
print("Pizza for everyone!")
}
}
}问题 4/12:这段代码是有效的 Swift 吗 —— 是或否?
func brushTeeth(useFloss: Bool = true) {
if floss {
print("I'm flossing first.")
}
print("I'm brushing my teeth.")
}问题 5/12:这段代码是有效的 Swift 吗 —— 是或否?
func runRace(distance: Int = 10) {
if distance < 5 {
print("This should be easy!")
} else if distance < 10 {
print("This should be a nice challenge.")
} else {
print("Let's do this!")
}
}问题 6/12:这段代码是有效的 Swift 吗 —— 是或否?
func playGolf(holes: String = 18) {
print("Let's play \(holes) holes of golf.")
}
playGolf()问题 7/12:这段代码是有效的 Swift 吗 —— 是或否?
func scoreGoal(overheadKick: Bool = false) {
if overheadKick {
print("Wow - amazing!")
} else {
print("Great goal!")
}
}
scoreGoal(overheadKick: true)问题 8/12:这段代码是有效的 Swift 吗 —— 是或否?
func goToWarp(speed: Int) {
if speed > 9 {
print("The engines cannae take any more!")
} else {
print("Going to warp \(speed)...")
}
}
goToWarp()问题 9/12:这段代码是有效的 Swift 吗 —— 是或否?
func takePicture(withFlash flash Bool = true) {
if flash {
print("I'm taking a photo with flash")
} else {
print("I'm taking a photo")
}
}问题 10/12:这段代码是有效的 Swift 吗 —— 是或否?
func parkCar(_ type: String, automatically: Bool = true) {
if automatically {
print("Nice - my \(type) parked itself!")
} else {
print("I guess I'll have to do it.")
}
}
parkCar("Tesla")问题 11/12:这段代码是有效的 Swift 吗 —— 是或否?
func fireArrow(skillLevel: Int = 1) -> String {
switch skillLevel {
case 1...3:
print("You missed the target.")
case 4...7:
print("You hit the target.")
default:
print("Great shot!")
}
}问题 12/12:这段代码是有效的 Swift 吗 —— 是或否?
func eatMeal(withDessert: Bool = true) {
if withDessert {
print("All the best meals include dessert.")
} else {
print("Bah... maybe next time.")
}
}
eatMeal(dessert: true)8.2 如何处理函数中的错误
作者:Paul Hudson 2021 年 11 月 8 日 已针对 Xcode 16.4 更新
错误总是层出不穷,比如你想要读取的文件不存在,或者尝试下载的数据因为网络中断而失败。如果我们不能优雅地处理这些错误,代码就会崩溃,所以 Swift 要求我们处理错误 —— 或者至少要承认错误可能会发生。
这需要三个步骤:
- 告诉 Swift 可能会发生哪些错误。
- 编写一个在错误发生时能标记错误的函数。
- 调用该函数,并处理可能发生的任何错误。
让我们来看一个完整的例子:如果用户让我们检查他们的密码强度,当密码太短或过于明显时,我们会标记一个严重错误。
首先,我们需要定义可能发生的错误。这意味着要创建一个新的枚举,它基于 Swift 现有的 Error 类型,如下所示:
enum PasswordError: Error {
case short, obvious
}这表示密码可能有两种错误:short(太短)和 obvious(过于明显)。这里并没有定义它们具体的含义,只是表明它们存在。
第二步是编写一个会触发这些错误之一的函数。在 Swift 中,当错误被触发 —— 或者说被 “抛出” 时,意味着函数发生了严重问题,不会继续正常执行,也不会返回任何值。
在我们的例子中,我们要编写一个检查密码强度的函数:如果密码非常糟糕 —— 少于 5 个字符或者是极其常见的密码,我们会立即抛出错误,而对于其他所有字符串,我们会返回 “OK”“Good” 或 “Excellent” 的评级。
下面是 Swift 中的实现:
func checkPassword(_ password: String) throws -> String {
if password.count < 5 {
throw PasswordError.short
}
if password == "12345" {
throw PasswordError.obvious
}
if password.count < 8 {
return "OK"
} else if password.count < 10 {
return "Good"
} else {
return "Excellent"
}
}让我们来详细分析一下:
- 如果一个函数可能抛出错误但自身不处理,你必须在返回类型前用
throws标记该函数。 - 我们不会具体指定函数抛出的错误类型,只表明它可能会抛出错误。
- 被标记为
throws并不意味着函数 “一定会” 抛出错误,只是 “可能会”。 - 当需要抛出错误时,我们会编写
throw语句,后面跟一个我们定义的PasswordError枚举成员。执行这条语句会立即退出当前函数,这意味着该函数不会返回原本应返回的字符串。 - 如果没有抛出错误,函数必须正常运行 —— 它需要返回一个字符串。
这就完成了抛出错误的第二步:我们定义了可能发生的错误,然后编写了一个使用这些错误的函数。
最后一步是运行函数并处理可能发生的任何错误。Swift Playgrounds 在错误处理方面相当宽松,因为它们主要用于学习,但在实际的 Swift 项目中,你会发现有三个步骤:
- 用
do开始一个可能抛出错误的代码块。 - 用
try调用一个或多个可能抛出错误的函数。 - 用
catch处理任何抛出的错误。
用伪代码表示如下:
do {
try someRiskyWork()
} catch {
print("在这里处理错误")
}如果我们想用我们当前的 checkPassword() 函数来实现,我们可以这样写:
let string = "12345"
do {
let result = try checkPassword(string)
print("密码评级:\(result)")
} catch {
print("出现了错误。")
}如果 checkPassword() 函数正常工作,它会将一个值返回到 result 中,然后这个值会被打印出来。但如果有任何错误被抛出(在这种情况下确实会有),密码评级信息将永远不会被打印 —— 程序执行会立即跳转到 catch 块。
这段代码中有几个部分值得讨论,但我想重点关注最重要的一个:try。在调用所有可能抛出错误的函数之前,必须写上 try,它是一个视觉信号,向开发者表明如果发生错误,常规的代码执行将会被中断。
当你使用 try 时,你需要在一个 do 块内部,并且确保有一个或多个 catch 块能够处理任何错误。在某些情况下,你可以使用 try! 作为替代,它不需要 do 和 catch,但如果抛出错误,你的代码将会崩溃 —— 你应该很少使用它,只有在你绝对确定不会抛出错误的情况下才使用。
在捕获错误时,你必须始终有一个能够处理各种错误的 catch 块。不过,如果你愿意,你也可以捕获特定的错误:
let string = "12345"
do {
let result = try checkPassword(string)
print("密码评级:\(result)")
} catch PasswordError.short {
print("请使用更长的密码。")
} catch PasswordError.obvious {
print("我的行李箱密码也是这个组合!")
} catch {
print("出现了错误。")
}随着你的深入学习,你会发现抛出函数被广泛应用于许多苹果自己的框架中,所以即使你自己可能不常创建它们,你至少需要知道如何安全地 “使用” 它们。
提示: 苹果抛出的大多数错误都提供了有意义的信息,必要时你可以展示给用户。Swift 通过在你的 catch 块中自动提供一个 error 值来实现这一点,通常可以通过 error.localizedDescription 来查看具体发生了什么。
【可选阅读】何时应该编写抛出函数?
作者:Paul Hudson 2020 年 6 月 5 日 已针对 Xcode 16.4 更新
Swift 中的抛出函数是那些可能遇到无法处理或不愿处理的错误的函数。这并不意味着它们 “一定会” 抛出错误,只是说它们有可能会。因此,Swift 会确保我们在使用它们时非常谨慎,以便处理任何确实发生的错误。
但当你编写代码时,你可能会想:“这个函数应该抛出它遇到的任何错误,还是应该自己处理这些错误呢?” 这是很常见的想法,说实话,没有唯一的答案 —— 你可以在函数内部处理错误(从而使它不是一个抛出函数),你可以将所有错误返回给调用该函数的对象(称为 “错误传播” 或有时称为 “错误冒泡”),你甚至可以在函数中处理一些错误,而将另一些错误返回。所有这些都是有效的解决方案,而且你在某个时候都会用到它们。
当你刚刚开始学习时,我建议你大多数时候避免使用抛出函数。一开始它们可能会让人觉得有点笨拙,因为你需要确保在使用函数的所有地方都处理好错误,所以感觉它几乎有点 “传染性”—— 突然之间,你的代码中有几个地方需要处理错误,如果这些错误进一步冒泡,这种 “传染性” 就会蔓延。
所以,学习的时候从小处着手:减少抛出函数的数量,然后从那里逐步扩展。随着时间的推移,你会更好地掌握错误管理,使程序流程更加顺畅,并且你会更有信心添加抛出函数。
【可选阅读】为什么 Swift 要求我们在每个抛出函数前使用 try?
作者:Paul Hudson 2020 年 5 月 28 日 已针对 Xcode 16.4 更新
使用 Swift 的抛出函数依赖于三个独特的关键字:do、try 和 catch。我们需要这三个关键字才能调用一个抛出函数,这有点不寻常 —— 大多数其他语言只使用两个,因为它们不需要在每个抛出函数前写 try。
Swift 之所以不同,原因相当简单:通过强制我们在每个抛出函数前使用 try,我们明确地承认了代码中哪些部分可能会导致错误。如果你在一个 do 块中有多个抛出函数,这一点特别有用,如下所示:
do {
try throwingFunction1()
nonThrowingFunction1()
try throwingFunction2()
nonThrowingFunction2()
try throwingFunction3()
} catch {
// 处理错误
}正如你所看到的,使用 try 可以清楚地表明第一个、第三个和第五个函数调用可能会抛出错误,而第二个和第四个则不会。
【练习题】编写抛出函数
问题 1/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum BuildingError: Error {
case tooHigh
case tooLow
}
func constructBuilding(floors: Int) throws {
if height < 10 {
throw BuildingError.tooLow
} else if height > 500 {
throw BuildingError.tooHigh
}
print("太完美了——我们开始建造吧!")
}问题 2/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum PlayError: Error {
case cheating
case noPlayers
}
func playGame(name: String, cheat: Bool = false) throws {
if cheat {
throw PlayError.cheating
} else {
print("让我们玩一个名为\(name)的游戏……")
}
}问题 3/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum AgeError: Error {
case underAge
case unknownAge
}
func buyAlcohol(age: Int) {
if age >= 18 {
print("可以。")
} else {
throw AgeError.underAge
}
}问题 4/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum PizzaErrors: Error {
case hasPineapple
}
func makePizza(type: String) throws {
if type != "夏威夷披萨" {
print("你的披萨将在10分钟后准备好。")
} else {
throw PizzaErrors.hasPineapple
}
}问题 5/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum MeasureError: Error {
case unknownItem
}
func measure(itemName: String) throws -> Double {
switch itemName {
case "书架":
return 2.0
case "椅子":
return 1.0
case "孩子":
return 1.3
case "成人":
return 1.75
}
}问题 6/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum ChargeError {
case noCable
case noPower
}
func chargePhone(atHome: Bool) throws {
if atHome {
print("手机正在充电……")
} else {
throw ChargeError.noPower
}
}问题 7/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum WifiError: Error {
case noNetwork
case noSignal
}
func connectToWifi(_ password: String) throws {
if password == "" {
throw WifiError.badPassword
}
print("你已连接。")
}问题 8/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum PrintError: Error {
case invalidCount
}
func printPages(text: String, count: Int) throws {
if count <= 0 {
throw PrintError.invalidCount
} else {
for _ in 1...count {
print("正在打印\(text)……")
}
}
}问题 9/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum LoginError: Error {
case unknownUser
}
func authenticate(username: String) throws {
if username == "Anonymous" {
throw LoginError.unknownUser
}
print("欢迎,\(username)!")
}问题 10/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum BookErrors: Error {
case tooFewPages
case tooManyPages
}
func writeBook(title: String, pages: Int) throws {
switch pages {
case 0...50:
throw BookErrors.tooFewPages
case 51...400:
print("太完美了!我要写\(title)……")
default:
throw BookErrors.tooManyPages
}
}问题 11/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum ArrayError: Error {
case negateIndex
}
func readItem(_ index: Int, from array: [String]) -> String {
if index < 0 {
throw ArrayError.negateIndex
}
return array[index]
}问题 12/12:这段代码是有效的 Swift 代码吗 —— 对还是错?
enum CatProblems: Error {
case notACat
case unfriendly
}
func strokeCat(_ name: String) throws {
if name == "Mr Bitey" {
throw CatProblems.unfriendly
} else if name == "Lassie" {
throw CatProblems.notACat
} else {
print("你抚摸了\(name)。")
}
}【练习题】运行抛出函数
问题 1/6:以下哪项是正确的?
- 选项 1: 使用
do开始一段调用抛出函数的代码。 - 选项 2: 你不需要捕获所有错误。
问题 2/6:以下哪项是正确的?
- 选项 1: Swift 不会让你意外地运行一个可能抛出错误的函数。
- 选项 2: 当安全时,Swift 允许你跳过添加
catch块。
问题 3/6:以下哪项是正确的?
- 选项 1: 你可以将任何函数标记为抛出函数。
- 选项 2: 在你的应用程序中只能定义一个错误枚举。
问题 4/6:以下哪项是正确的?
- 选项 1: 如果有任何错误被抛出,执行会立即跳转到
catch块。 - 选项 2: 你应该将所有函数都标记为抛出函数。
问题 5/6:以下哪项是正确的?
- 选项 1: 抛出函数必须用
throws标记。 - 选项 2: 在一个
do块中只能调用一个抛出函数。
问题 6/6:以下哪项是正确的?
- 选项 1: 抛出函数不能返回值。
- 选项 2: 必须使用
try调用抛出函数。
8.3 总结:函数
作者:Paul Hudson 2021 年 10 月 7 日 已针对 Xcode 16.4 更新*
在前几章中,我们已经介绍了很多关于函数的内容,现在来回顾一下:
- 函数通过将代码块分割出来并赋予名称,让我们可以轻松地重用代码。
- 所有函数都以
func开头, 后面紧跟函数的名称, 函数体包含在左右大括号内。 - 我们可以添加参数使函数更灵活 —— 逐个列出参数,用逗号分隔:参数名称,然后是冒号,再然后是参数类型。
- 你可以控制这些参数名称在外部的使用方式,要么使用自定义的外部参数名,要么使用下划线来禁用该参数的外部名称。
- 如果你认为某些参数值会被反复使用,可以给它们设置默认值,这样函数编写起来更简洁,并且在默认情况下就能智能地工作。
- 函数可以根据需要返回一个值,但如果你想从函数返回多个数据,应该使用元组。元组包含多个命名元素,但它在某种程度上不像字典那样灵活 —— 你需要具体列出每个元素及其类型。
- 函数可能会抛出错误:你可以创建一个枚举来定义可能发生的错误,在函数内部根据需要抛出这些错误,然后在调用处使用
do、try和catch来处理它们。
8.4 检查点 4
作者:Paul Hudson 2021 年 10 月 7 日 已针对 Xcode 16.4 更新
掌握了函数的知识后,是时候尝试一个小的编程挑战了。别担心,它没那么难,但你可能需要花点时间思考并想出解决办法。和往常一样,如果你需要,我会给你一些提示。
挑战是这样的:编写一个函数,接受一个 1 到 10,000 之间的整数,并返回该数的整数平方根。这听起来很简单,但有一些注意事项:
- 你不能使用 Swift 内置的
sqrt()函数或类似的函数 —— 你需要自己找到平方根。 - 如果这个数小于 1 或大于 10,000,你应该抛出一个 “超出范围” 的错误。
- 你只需要考虑整数平方根 —— 例如,不用担心 3 的平方根是 1.732 这样的情况。
- 如果你找不到平方根,抛出一个 “无平方根” 的错误。
提醒一下,如果你有数字 X,X 的平方根将是另一个数字,当它乘以自身时会得到 X。所以,9 的平方根是 3,因为 3×3=9,25 的平方根是 5,因为 5×5=25。
过一会儿我会给你一些提示,但和往常一样,我鼓励你先自己尝试 —— 努力回忆知识的运作方式,并且经常需要重新查阅,这是取得进步的有效方法。
以下是一些提示:
- 这是一个你应该 “暴力破解” 的问题 —— 创建一个循环,在循环内部进行乘法运算,寻找你传入的整数。
- 10,000(我希望你处理的最大数字)的平方根是 100,所以你的循环应该在那里停止。
- 如果你循环结束还没有找到匹配的数,就抛出 “无平方根” 错误。
- 你可以为 “小于 1” 和 “大于 10,000” 定义不同的超出范围错误,如果你想的话,但实际上没必要 —— 只定义一个就可以了。