Swift 函数
函数把一段完成特定任务的代码封装起来,给它起一个名字,之后就可以反复调用。
Swift 的函数功能比多数语言丰富:参数可以有外部标签、默认值、可变个数,还可以把函数本身当作参数和返回值。
本文从最基本的定义与调用讲起,再逐步覆盖标签、默认值、inout、函数类型、嵌套函数,以及 @escaping、@autoclosure、@discardableResult 三个常用属性。
定义与调用
用 func 关键字定义函数,参数写在括号里,返回类型用 -> 指向。
下面的函数接收一个字符串,返回一句问候语。
实例
// 定义函数:首参数前写 _,表示调用时不写标签
func greet(_ name: String) -> String {
return "Hello, \(name)!"
}
// 调用时直接传值,不写 name:
print(greet("Runoob"))
以上代码执行输出结果为:
Hello, Runoob!
注意:Swift 3 起(SE-0046),所有参数默认都带外部标签,第一个参数也不例外。所以 func greet(name: String) 调用时必须写成 greet(name: "Runoob"),直接写 greet("Runoob") 会报 missing argument label 'name:' in call。「第一个参数默认没有外部标签」是 Swift 2 的旧规则,早已作废。
参数标签与参数名
参数可以有两个名字:写在前面的是外部标签(调用时用),写在后面的是内部参数名(函数体里用)。
这种设计让调用处读起来像一句英文,同时函数体内部仍可使用简短的变量名。
实例
// person 是外部标签,name 是内部参数名
func greet(person name: String, from city: String) -> String {
return "\(name) 来自 \(city)"
}
// 调用时必须写 person: 与 from:
print(greet(person: "Runoob", from: "www.runoob.com"))
以上代码执行输出结果为:
Runoob 来自 www.runoob.com
如果调用时不想写标签,就在参数名前加下划线 _ 显式省略。
| 参数位置与写法 | 外部标签 | 声明与调用示例 |
|---|---|---|
| 普通写法(首参数也一样) | 参数名本身 | func f(x: Int) → f(x: 1) |
| 加 _ 省略标签 | 无 | func f(_ x: Int) → f(1) |
| 自定义外部标签 | 自定义标签 | func f(label x: Int) → f(label: 1) |
| 第二个及之后的参数 | 参数名本身 | func f(a: Int, b: Int) → f(a: 1, b: 2) |
| 带默认值的参数 | 参数名本身 | func f(a: Int = 1) → f(a: 2) |
参数默认值
给参数写上默认值后,调用时可以省略该参数;有默认值的参数通常放在参数列表末尾。
实例
// scheme 有默认值,调用时可以省略
func makeURL(host: String, scheme: String = "https") -> String {
return "\(scheme)://\(host)"
}
print(makeURL(host: "www.runoob.com"))
print(makeURL(host: "www.runoob.com", scheme: "http"))
以上代码执行输出结果为:
https://www.runoob.com http://www.runoob.com
注意:带默认值的参数会自动获得与参数名相同的外部标签。即使它排在第一位,调用时也要写标签:
实例
// name 有默认值,自动获得外部标签 name
func greet(name: String = "Runoob") -> String {
return "Hello, \(name)!"
}
print(greet())
print(greet(name: "RUNOOB"))
以上代码执行输出结果为:
Hello, Runoob! Hello, RUNOOB!
可变参数
在参数类型后加三个点 ...,表示这个参数可以接收任意个同类型的值。
可变参数在函数体内是一个数组,可以不传,此时数组为空。
实例
// numbers 是可变参数,可以传任意个 Int
func sum(_ numbers: Int...) -> Int {
var total = 0
for n in numbers {
total += n
}
return total
}
print(sum(1, 2, 3, 4, 5))
print(sum())
以上代码执行输出结果为:
15 0
一个函数最多只能有一个可变参数,而且它不能带外部标签以外的特殊写法。
inout 参数
函数参数默认是常量,函数内改不了,也影响不到外部变量。
想让函数修改传入的变量,把参数声明为 inout,调用时在变量前加 &。
实例
// 两个参数都加 inout,函数内修改会影响外部变量
func swapValues(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
swapValues(&x, &y)
print("x=\(x), y=\(y)")
以上代码执行输出结果为:
x=2, y=1
注意:inout 参数要求独占访问。把同一个变量同时传给两个 inout 参数,例如 swapValues(&x, &x),编译器会直接报错:inout arguments are not allowed to alias each other(inout 参数之间不允许互为别名)。
函数类型
每个函数都有自己的类型,由参数类型和返回类型组成,例如 (Int, Int) -> Int。
函数类型可以像普通类型一样用来声明变量。
实例
// 声明一个函数类型变量:接收两个 Int,返回 Int
let mathFunction: (Int, Int) -> Int = { $0 + $1 }
print(mathFunction(3, 4))
以上代码执行输出结果为:
7
返回类型为 Void 的函数,类型写作 () -> Void,也可以省略箭头写成 () -> ()。
函数作为参数与返回值
函数类型既然是类型,自然可以当参数传进去,也可以当结果返回。
作为参数
调用者把「怎么做」交给函数,函数只负责在合适的时机调用它。
实例
// operation 是一个函数类型的参数
func apply(_ value: Int, using operation: (Int) -> Int) -> Int {
return operation(value)
}
// 传入一个把参数平方的闭包
print(apply(5, using: { $0 * $0 }))
以上代码执行输出结果为:
25
作为返回值
返回一个函数,可以让调用方在稍后决定具体用哪种运算。
实例
// 返回类型是 (Int, Int) -> Int
func chooseOperation(_ op: String) -> (Int, Int) -> Int {
switch op {
case "add":
return { $0 + $1 }
case "mul":
return { $0 * $1 }
default:
return { _, _ in 0 }
}
}
let add = chooseOperation("add")
print(add(10, 20))
以上代码执行输出结果为:
30
嵌套函数
函数可以定义在另一个函数内部,这种函数叫嵌套函数。
嵌套函数默认只在外部函数内部可见,常用来把辅助逻辑收拢在一起。
实例
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
// stepForward 与 stepBackward 是嵌套在函数内部的函数
func stepForward(_ input: Int) -> Int { return input + 1 }
func stepBackward(_ input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
print(chooseStepFunction(backward: true)(10))
print(chooseStepFunction(backward: false)(10))
以上代码执行输出结果为:
9 11
@escaping 逃逸闭包
函数类型的参数默认是非逃逸的:闭包必须在函数返回前执行完。
如果闭包要被保存起来、在函数返回之后才调用,就必须标注 @escaping。
实例
// 用类来保存闭包,避免全局可变状态
class HandlerStore {
var stored: (() -> Void)?
// @escaping 表示闭包会逃出函数作用域,被保存下来
func store(_ action: @escaping () -> Void) {
stored = action
}
}
let store = HandlerStore()
store.store { print("逃逸闭包稍后执行") }
store.stored?()
以上代码执行输出结果为:
逃逸闭包稍后执行
逃逸闭包常常在保存 self 时引发循环引用,解决办法见第 33 篇《Swift 自动引用计数》中的捕获列表。
@autoclosure 自动闭包
@autoclosure 会自动把传入的表达式包装成一个闭包,调用处因此可以少写一层大括号。
它的价值在于延迟求值:只有真正用到时,表达式才会被计算。
实例
// @autoclosure 把表达式自动包装成闭包
func logIfTrue(_ condition: @autoclosure () -> Bool) {
if condition() {
print("条件为真")
}
}
// 调用时直接写表达式,不需要写成 { 1 < 2 }
logIfTrue(1 < 2)
以上代码执行输出结果为:
条件为真
标准库里的 && 与 || 就使用了这个特性,所以右边表达式只有在需要时才求值,短路效果才成立。
@discardableResult
函数有返回值时,调用却不接收,编译器会给出警告。
确实不需要返回值的地方,用 @discardableResult 标注函数,警告就消失了。
实例
// @discardableResult 允许调用者忽略返回值而不产生警告
@discardableResult
func increase(_ value: inout Int) -> Int {
value += 1
return value
}
var counter = 0
increase(&counter)
increase(&counter)
print(counter)
以上代码执行输出结果为:
2
常见问题
下面是函数参数与闭包属性中最容易混淆的几个问题。
调用时到底要不要写参数标签
看声明。Swift 3 起所有参数默认都用参数名当外部标签,第一个参数也不例外;只有声明里写了 _ 的位置才不需要写标签。
声明里写了 _ 就一定不写标签,写了自定义标签就必须用那个标签。
inout 和返回值该选哪个
函数只产出一个结果时,优先用返回值,语义更清楚,也没有副作用。
确实要修改调用方传入的变量,或者要同时改多个变量时,再用 inout。
为什么闭包参数有时必须加 @escaping
不加 @escaping 时,编译器保证闭包在函数返回前用完,这样才能做更严格的优化与检查。
一旦把闭包存进属性、放进数组,或交给异步任务,就必须显式标注 @escaping。
@autoclosure 会不会让代码难懂
会。调用处看不出这里有闭包,误用还可能让表达式被执行多次或延迟执行。
公共 API 中需要延迟求值时才用,日常代码里直接写闭包更直观。
