今天是我们学习函数的下半部分,这一部分我们先了解一下如何给函数的参数提供默认值,然后再一起学习如何处理函数中的错误,最后是总结。
如何为参数提供默认值
为函数添加参数可以让我们添加自定义点,这样函数就可以根据我们的需要对不同的数据进行操作。有时我们希望提供这些自定义点,以保持代码的灵活性,但有时你并不想考虑这些。
例如,我们之前看过这样一个函数:
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) ——那么默认值 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。
正如你所看到的,默认参数值可以让我们在函数中保持灵活性,而不会让函数在大多数情况下调用起来令人讨厌——只有当你需要一些不寻常的东西时,才需要发送一些参数。
何时使用函数的默认参数
默认参数通过为参数提供常用的默认值,让函数的调用变得更简单。因此,当我们想使用这些默认值调用函数时,我们可以完全忽略这些参数,就像它们不存在一样,我们的函数就会做正确的事情。当然,当我们需要自定义参数时,也可以对其进行更改。
Swift 开发人员经常使用默认参数,因为它们可以让我们专注于需要定期更改的重要部分。这确实有助于简化复杂的函数,使代码更易于编写。
例如,想象一下这样的寻路代码:
func findDirections(from: String, to: String, route: String = "fastest", avoidHighways: Bool = false) {
// code here
}
这样做的前提是,大多数情况下,人们会选择最快的路线来往于两个地点之间,而不会避开高速公路——这种合理的默认值在大多数情况下都可能有效,同时也为我们提供了在需要时提供自定义值的空间。
因此,我们可以通过以下三种方式调用同一个函数:
findDirections(from: "Shanghai", to: "Hangzhou")
findDirections(from: "Shanghai", to: "Hangzhou", route: "scenic")
findDirections(from: "Shanghai", to: "Hangzhou", route: "scenic", avoidHighways: true)
在大多数情况下,代码更短、更简单,但在我们需要的时候又能灵活运用——完美。
如何处理函数中的错误
出错是家常便饭,比如你想读取的文件不存在,或者你想下载的数据因为网络故障而下载失败。如果我们不能优雅地处理错误,那么我们的代码就会崩溃,所以 Swift 让我们处理错误,或者至少承认错误可能发生。
这需要三个步骤:
- 告诉 Swift 可能发生的错误。
- 编写一个可以在发生错误时标记错误的函数。
- 调用该函数,并处理任何可能发生的错误。
让我们举一个完整的例子:如果用户要求我们检查其密码的强度,如果密码太短或太明显,我们就会提示严重错误。
因此,我们需要先定义可能发生的错误。这意味着要在 Swift 现有的 Error 类型基础上创建一个新的枚举,就像下面这样:
enum PasswordError: Error {
case 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("Handle errors here")
}
如果我们想使用当前的 checkPassword() 函数编写 try,可以这样写:
let string = "12345"
do {
let result = try checkPassword(string)
print("Password rating: \(result)")
} catch {
print("There was an error.")
}
如果 checkPassword() 函数工作正常,它将返回一个值到 result 中,然后打印出来。但如果出现任何错误(本例中就会出现错误),则永远不会打印密码评级信息,执行过程会立即跳转到 catch 块。
这段代码中有几个不同的部分值得讨论,但我想重点谈谈其中最重要的部分:try。这段代码必须写在调用所有可能出错的函数之前,它是给开发人员的一个可视化信号,表明如果发生错误,正常的代码执行将被中断。
使用 try 时,需要在 do 代码块中,并确保有一个或多个 catch 代码块可以处理任何错误。在某些情况下,您可以使用另一种写法 try!,它不需要 do 和 catch,但如果出现错误,您的代码就会崩溃。
说到捕获错误,你必须始终有一个可以处理各种错误的 catch 块。不过,如果您愿意,也可以捕获特定的错误:
let string = "12345"
do {
let result = try checkPassword(string)
print("Password rating: \(result)")
} catch PasswordError.short {
print("Please use a longer password.")
} catch PasswordError.obvious {
print("I have the same combination on my luggage!")
} catch {
print("There was an error.")
}
随着学习的深入,你会发现抛出函数是如何嵌入到许多苹果自己的框架中的,因此,即使你可能不会自己创建太多的抛出函数,你至少需要知道如何安全地使用它们。
小提示:苹果抛出的大多数错误都会提供一条有意义的信息,你可以根据需要将其呈现给用户。Swift 通过在 catch 代码块中自动提供的错误值来提供这些信息,通常可以通过阅读 error.localizedDescription 来了解具体发生了什么。
何时应该编写抛出函数?
Swift 中的抛出函数是指那些遇到无法或不愿处理的错误的函数。这并不意味着它们会抛出错误,只是说它们有可能会抛出错误。因此,Swift 会确保我们在使用它们时小心谨慎,以应对可能发生的错误。
但在编写代码时,你很可能会想:”这个函数是否应该抛出遇到的错误,还是应该自己处理?这种情况很常见,老实说并没有唯一的答案——你可以在函数内部处理错误(从而使其不再是一个抛出函数),也可以将错误全部发送回调用函数的对象(称为 “错误传播(error propagation)”,有时也称为 “错误冒泡(bubbling up errors)”),甚至还可以在函数中处理一些错误,再将一些错误发送回去。所有这些都是有效的解决方案,你都会在某些时候用到。
在刚开始使用时,我建议你在大多数情况下避免使用抛出函数。一开始,它们会让人感觉有点笨拙,因为无论你在哪里使用函数,都需要确保所有的错误都得到了处理,所以这几乎有点 “传染”的感觉——突然间,你的代码中有好几个地方都需要处理错误,如果这些错误进一步扩散,那么 “传染 “就会蔓延开来。
因此,在学习过程中要从小处入手:减少抛出函数的数量,然后再向外扩展。随着时间的推移,你会更好地掌握管理错误的方法,从而保持程序的流畅,你也会对添加抛出函数更有信心。
为什么 Swift 让我们在每个抛出函数前都使用 try?
使用 Swift 的抛出函数依赖于三个独特的关键字:do、try 和 catch。我们需要同时使用这三个关键字才能调用一个抛出函数,这很不寻常——大多数其他语言只使用两个关键字,因为他们不需要在每个抛出函数前都写 try。
Swift 不同于其他语言的原因很简单:通过强制我们在每个抛出函数之前使用 try,我们明确承认了代码的哪些部分可能导致错误。如果在一个 do 代码块中有多个抛出函数,这一点就特别有用,就像下面这样:
do {
try throwingFunction1()
nonThrowingFunction1()
try throwingFunction2()
nonThrowingFunction2()
try throwingFunction3()
} catch {
// handle errors
}
正如你所看到的,使用 try 可以清楚地表明,第一、第三和第五次函数调用可以抛出错误,但第二和第四次不能。
总结:函数
在前面的章节中,我们已经介绍了很多关于函数的知识,现在让我们来回顾一下:
- 函数通过分割代码块并为其命名,让我们可以轻松重复使用代码。
- 所有函数都以单词 func 开头,然后是函数名称。函数的主体包含在大括号中。
- 我们可以添加参数,使函数更加灵活——用逗号分隔,逐个列出参数:参数名称,然后是冒号,然后是参数类型。
- 你可以控制外部如何使用这些参数名,可以使用自定义的外部参数名,也可以使用下划线禁用该参数的外部名称。
- 如果你认为某些参数值会重复使用,你可以为它们设置一个默认值,这样你的函数就可以减少代码编写量,并在默认情况下做明智的事情。
- 如果需要,函数可以返回一个值,但如果要从函数中返回多个数据,则应使用元组。元组可以容纳多个命名的元素,但它的限制与字典不同——你需要具体列出每个元素及其类型。
- 函数可以抛出错误:你可以创建一个枚举,定义可能会发生的错误,根据需要在函数内部抛出这些错误,然后使用 do、try 和 catch 在调用位置处理这些错误。
思考题
编写一个函数,使它能接收一个从1到10000的整数,并返回这个整数的整数平方根。要求:
- 不能使用sqrt()函数或类似的Swift内置函数
- 如果数字小于1或者大于10000,则使函数抛出“超限”错误。
- 只考虑整数平方根,不考虑小数。
- 如果没有整数平方根,则抛出“无根”错误。
答案可能有很多种,不用拘泥于我提供的代码,只要功能实现即可:
enum NumberError: Error {
case noRoot, outOfBounds
}
func sqrtOf(_ number: Int) throws -> Int {
if number > 10000 || number < 1 {
throw NumberError.outOfBounds
}
for i in 1...100 {
let x = i * i
if number == x {
return i
}
}
throw NumberError.noRoot
}
do {
let number = 100000
let result = try sqrtOf(number)
print("\(number)'s root is \(result).")
} catch NumberError.noRoot {
print("This number has no root.")
} catch NumberError.outOfBounds {
print("This number is out of bounds.")
}
好了,函数的内容就到此为止了,虽然看似很简单,但函数的用处非常大,希望大家能多多练习。
下一章,我们会开始学习闭包(Closure)。