Skip to content

第8天 默认值、抛出异常

今天你将学习如何处理函数中的错误。这听起来可能有些消极,但就像约翰・列侬说的:“生活就是当你忙于制定其他计划时发生的事情”—— 没人愿意遇到问题,但生活总会不经意间出现各种问题!

幸运的是,Swift 让错误处理变得简单直观且相对可靠:它要求我们处理错误,或者至少承认错误可能会发生。如果你不至少尝试妥善处理错误,你的代码根本无法编译。

今天你需要完成两个教程,在这些教程中,你将学习参数的默认值和抛出函数,然后我们会总结函数的相关知识并完成 checkpoint 4。完成每个视频后,如果你想了解更多细节,有一段可选的额外阅读内容,还有一个简短的测试来帮助你确认是否理解了所教授的内容。

8.1 如何为参数提供默认值

作者:Paul Hudson 2021 年 10 月 7 日 已针对 Xcode 16.4 更新

向函数添加参数使我们能够增加自定义点,这样函数就可以根据我们的需求对不同的数据进行操作。有时我们希望保留这些自定义点以保持代码的灵活性,但其他时候你可能不想考虑这些 —— 你十有八九都想要同样的结果。

例如,之前我们看过这个函数:

swift
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。

代码如下所示:

swift
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。

实际上,我们之前使用过的一些代码中就有默认参数的例子:

swift
var characters = ["Lana", "Pam", "Ray", "Sterling"]
print(characters.count)
characters.removeAll()
print(characters.count)

这段代码向数组中添加一些字符串,打印其计数,然后删除所有字符串并再次打印计数。

作为一种性能优化,Swift 会给数组分配刚好足够的内存来容纳其元素,再加上一点点额外的内存,以便它们可以有一定的增长空间。如果向数组中添加更多元素,Swift 会自动分配更多的内存,这样可以尽可能减少浪费。

当我们调用removeAll()时,Swift 会自动删除数组中的所有元素,然后释放分配给数组的所有内存。这通常是你想要的,毕竟你删除这些对象是有原因的。但有时候 —— 只是有时候 —— 你可能马上要向数组中添加很多新元素,所以这个函数还有第二种形式,它在删除元素的同时保留之前的容量:

swift
characters.removeAll(keepingCapacity: true)

这是通过使用默认参数值来实现的:keepingCapacity是一个布尔值,默认值为 false,所以它在默认情况下会做合理的事情,同时也让我们可以在需要保留数组现有容量的时候传入 true。

如你所见,默认参数值让我们能够在保持函数灵活性的同时,不会让大多数时候的调用变得麻烦 —— 你只需要在需要一些特殊情况时传入某些参数。

【可选阅读】何时为函数使用默认参数

作者:Paul Hudson 2024 年 4 月 16 日 已针对 Xcode 16.4 更新

默认参数让我们可以为参数提供常见的默认值,从而使函数更易于调用。因此,当我们想要使用这些默认值调用函数时,我们可以完全忽略这些参数 —— 就好像它们不存在一样 —— 而我们的函数会正常工作。当然,当我们想要一些自定义的东西时,这些参数也可以供我们修改。

Swift 开发者经常使用默认参数,因为它们让我们可以专注于那些确实需要经常更改的重要部分。这确实有助于简化复杂的函数,并使你的代码更易于编写。

例如,想象一些路线查找代码如下:

swift
func findDirections(from: String, to: String, route: String = "fastest", avoidHighways: Bool = false) {
    // 代码在这里
}

它假设大多数时候人们希望通过最快的路线在两个地点之间驾车,并且不避开高速公路 —— 这些合理的默认值可能在大多数情况下都适用,同时也让我们能够在需要时提供自定义值。

因此,我们可以用以下三种方式中的任何一种调用同一个函数:

swift
findDirections(from: "London", to: "Glasgow")
findDirections(from: "London", to: "Glasgow", route: "scenic")
findDirections(from: "London", to: "Glasgow", route: "scenic", avoidHighways: true)

大多数时候代码更简短、更简单,但在需要时又有灵活性 —— 非常完美。

【练习题】默认参数

问题 1/12:这段代码是有效的 Swift 吗 —— 是或否?

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 吗 —— 是或否?

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 吗 —— 是或否?

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 吗 —— 是或否?

swift
func brushTeeth(useFloss: Bool = true) {
    if floss {
        print("I'm flossing first.")
    }
    print("I'm brushing my teeth.")
}

问题 5/12:这段代码是有效的 Swift 吗 —— 是或否?

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 吗 —— 是或否?

swift
func playGolf(holes: String = 18) {
    print("Let's play \(holes) holes of golf.")
}
playGolf()

问题 7/12:这段代码是有效的 Swift 吗 —— 是或否?

swift
func scoreGoal(overheadKick: Bool = false) {
    if overheadKick {
        print("Wow - amazing!")
    } else {
        print("Great goal!")
    }
}
scoreGoal(overheadKick: true)

问题 8/12:这段代码是有效的 Swift 吗 —— 是或否?

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 吗 —— 是或否?

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 吗 —— 是或否?

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 吗 —— 是或否?

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 吗 —— 是或否?

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 要求我们处理错误 —— 或者至少要承认错误可能会发生。

这需要三个步骤:

  1. 告诉 Swift 可能会发生哪些错误。
  2. 编写一个在错误发生时能标记错误的函数。
  3. 调用该函数,并处理可能发生的任何错误。

让我们来看一个完整的例子:如果用户让我们检查他们的密码强度,当密码太短或过于明显时,我们会标记一个严重错误。

首先,我们需要定义可能发生的错误。这意味着要创建一个新的枚举,它基于 Swift 现有的 Error 类型,如下所示:

swift
enum PasswordError: Error {
    case short, obvious
}

这表示密码可能有两种错误:short(太短)和 obvious(过于明显)。这里并没有定义它们具体的含义,只是表明它们存在。

第二步是编写一个会触发这些错误之一的函数。在 Swift 中,当错误被触发 —— 或者说被 “抛出” 时,意味着函数发生了严重问题,不会继续正常执行,也不会返回任何值。

在我们的例子中,我们要编写一个检查密码强度的函数:如果密码非常糟糕 —— 少于 5 个字符或者是极其常见的密码,我们会立即抛出错误,而对于其他所有字符串,我们会返回 “OK”“Good” 或 “Excellent” 的评级。

下面是 Swift 中的实现:

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"
    }
}

让我们来详细分析一下:

  1. 如果一个函数可能抛出错误但自身不处理,你必须在返回类型前用 throws 标记该函数。
  2. 我们不会具体指定函数抛出的错误类型,只表明它可能会抛出错误。
  3. 被标记为 throws 并不意味着函数 “一定会” 抛出错误,只是 “可能会”。
  4. 当需要抛出错误时,我们会编写 throw 语句,后面跟一个我们定义的 PasswordError 枚举成员。执行这条语句会立即退出当前函数,这意味着该函数不会返回原本应返回的字符串。
  5. 如果没有抛出错误,函数必须正常运行 —— 它需要返回一个字符串。

这就完成了抛出错误的第二步:我们定义了可能发生的错误,然后编写了一个使用这些错误的函数。

最后一步是运行函数并处理可能发生的任何错误。Swift Playgrounds 在错误处理方面相当宽松,因为它们主要用于学习,但在实际的 Swift 项目中,你会发现有三个步骤:

  1. do 开始一个可能抛出错误的代码块。
  2. try 调用一个或多个可能抛出错误的函数。
  3. catch 处理任何抛出的错误。

用伪代码表示如下:

swift
do {
    try someRiskyWork()
} catch {
    print("在这里处理错误")
}

如果我们想用我们当前的 checkPassword() 函数来实现,我们可以这样写:

swift
let string = "12345"

do {
    let result = try checkPassword(string)
    print("密码评级:\(result)")
} catch {
    print("出现了错误。")
}

如果 checkPassword() 函数正常工作,它会将一个值返回到 result 中,然后这个值会被打印出来。但如果有任何错误被抛出(在这种情况下确实会有),密码评级信息将永远不会被打印 —— 程序执行会立即跳转到 catch 块。

这段代码中有几个部分值得讨论,但我想重点关注最重要的一个:try。在调用所有可能抛出错误的函数之前,必须写上 try,它是一个视觉信号,向开发者表明如果发生错误,常规的代码执行将会被中断。

当你使用 try 时,你需要在一个 do 块内部,并且确保有一个或多个 catch 块能够处理任何错误。在某些情况下,你可以使用 try! 作为替代,它不需要 docatch,但如果抛出错误,你的代码将会崩溃 —— 你应该很少使用它,只有在你绝对确定不会抛出错误的情况下才使用。

在捕获错误时,你必须始终有一个能够处理各种错误的 catch 块。不过,如果你愿意,你也可以捕获特定的错误:

swift
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 的抛出函数依赖于三个独特的关键字:dotrycatch。我们需要这三个关键字才能调用一个抛出函数,这有点不寻常 —— 大多数其他语言只使用两个,因为它们不需要在每个抛出函数前写 try

Swift 之所以不同,原因相当简单:通过强制我们在每个抛出函数前使用 try,我们明确地承认了代码中哪些部分可能会导致错误。如果你在一个 do 块中有多个抛出函数,这一点特别有用,如下所示:

swift
do {
    try throwingFunction1()
    nonThrowingFunction1()
    try throwingFunction2()
    nonThrowingFunction2()
    try throwingFunction3()
} catch {
    // 处理错误
}

正如你所看到的,使用 try 可以清楚地表明第一个、第三个和第五个函数调用可能会抛出错误,而第二个和第四个则不会。

【练习题】编写抛出函数

问题 1/12:这段代码是有效的 Swift 代码吗 —— 对还是错?

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 代码吗 —— 对还是错?

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 代码吗 —— 对还是错?

swift
enum AgeError: Error {
	case underAge
	case unknownAge
}
func buyAlcohol(age: Int) {
	if age >= 18 {
		print("可以。")
	} else {
		throw AgeError.underAge
	}
}

问题 4/12:这段代码是有效的 Swift 代码吗 —— 对还是错?

swift
enum PizzaErrors: Error {
	case hasPineapple
}
func makePizza(type: String) throws {
	if type != "夏威夷披萨" {
		print("你的披萨将在10分钟后准备好。")
	} else {
		throw PizzaErrors.hasPineapple
	}
}

问题 5/12:这段代码是有效的 Swift 代码吗 —— 对还是错?

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 代码吗 —— 对还是错?

swift
enum ChargeError {
	case noCable
	case noPower
}
func chargePhone(atHome: Bool) throws {
	if atHome {
		print("手机正在充电……")
	} else {
		throw ChargeError.noPower
	}
}

问题 7/12:这段代码是有效的 Swift 代码吗 —— 对还是错?

swift
enum WifiError: Error {
	case noNetwork
	case noSignal
}
func connectToWifi(_ password: String) throws {
	if password == "" {
		throw WifiError.badPassword
	}
	print("你已连接。")
}

问题 8/12:这段代码是有效的 Swift 代码吗 —— 对还是错?

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 代码吗 —— 对还是错?

swift
enum LoginError: Error {
	case unknownUser
}
func authenticate(username: String) throws {
	if username == "Anonymous" {
		throw LoginError.unknownUser
	}
	print("欢迎,\(username)!")
}

问题 10/12:这段代码是有效的 Swift 代码吗 —— 对还是错?

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 代码吗 —— 对还是错?

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 代码吗 —— 对还是错?

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开头, 后面紧跟函数的名称, 函数体包含在左右大括号内。
  • 我们可以添加参数使函数更灵活 —— 逐个列出参数,用逗号分隔:参数名称,然后是冒号,再然后是参数类型。
  • 你可以控制这些参数名称在外部的使用方式,要么使用自定义的外部参数名,要么使用下划线来禁用该参数的外部名称。
  • 如果你认为某些参数值会被反复使用,可以给它们设置默认值,这样函数编写起来更简洁,并且在默认情况下就能智能地工作。
  • 函数可以根据需要返回一个值,但如果你想从函数返回多个数据,应该使用元组。元组包含多个命名元素,但它在某种程度上不像字典那样灵活 —— 你需要具体列出每个元素及其类型。
  • 函数可能会抛出错误:你可以创建一个枚举来定义可能发生的错误,在函数内部根据需要抛出这些错误,然后在调用处使用dotrycatch来处理它们。

8.4 检查点 4

作者:Paul Hudson 2021 年 10 月 7 日 已针对 Xcode 16.4 更新

掌握了函数的知识后,是时候尝试一个小的编程挑战了。别担心,它没那么难,但你可能需要花点时间思考并想出解决办法。和往常一样,如果你需要,我会给你一些提示。

挑战是这样的:编写一个函数,接受一个 1 到 10,000 之间的整数,并返回该数的整数平方根。这听起来很简单,但有一些注意事项:

  1. 你不能使用 Swift 内置的sqrt()函数或类似的函数 —— 你需要自己找到平方根。
  2. 如果这个数小于 1 或大于 10,000,你应该抛出一个 “超出范围” 的错误。
  3. 你只需要考虑整数平方根 —— 例如,不用担心 3 的平方根是 1.732 这样的情况。
  4. 如果你找不到平方根,抛出一个 “无平方根” 的错误。

提醒一下,如果你有数字 X,X 的平方根将是另一个数字,当它乘以自身时会得到 X。所以,9 的平方根是 3,因为 3×3=9,25 的平方根是 5,因为 5×5=25。

过一会儿我会给你一些提示,但和往常一样,我鼓励你先自己尝试 —— 努力回忆知识的运作方式,并且经常需要重新查阅,这是取得进步的有效方法。

以下是一些提示:

  • 这是一个你应该 “暴力破解” 的问题 —— 创建一个循环,在循环内部进行乘法运算,寻找你传入的整数。
  • 10,000(我希望你处理的最大数字)的平方根是 100,所以你的循环应该在那里停止。
  • 如果你循环结束还没有找到匹配的数,就抛出 “无平方根” 错误。
  • 你可以为 “小于 1” 和 “大于 10,000” 定义不同的超出范围错误,如果你想的话,但实际上没必要 —— 只定义一个就可以了。

本站使用 VitePress 制作