现在位置: 首页 > Swift 教程 > 正文

Swift 函数

函数把一段完成特定任务的代码封装起来,给它起一个名字,之后就可以反复调用。

Swift 的函数功能比多数语言丰富:参数可以有外部标签、默认值、可变个数,还可以把函数本身当作参数和返回值。

本文从最基本的定义与调用讲起,再逐步覆盖标签、默认值、inout、函数类型、嵌套函数,以及 @escaping@autoclosure@discardableResult 三个常用属性。


定义与调用

func 关键字定义函数,参数写在括号里,返回类型用 -> 指向。

下面的函数接收一个字符串,返回一句问候语。

实例

import Foundation

// 定义函数:首参数前写 _,表示调用时不写标签
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 的旧规则,早已作废。


参数标签与参数名

参数可以有两个名字:写在前面的是外部标签(调用时用),写在后面的是内部参数名(函数体里用)。

Swift 参数标签三种写法对照:下划线省略标签、自定义标签、标签与参数名同名

这种设计让调用处读起来像一句英文,同时函数体内部仍可使用简短的变量名。

实例

import Foundation

// 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)

参数默认值

给参数写上默认值后,调用时可以省略该参数;有默认值的参数通常放在参数列表末尾。

实例

import Foundation

// 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

注意:带默认值的参数会自动获得与参数名相同的外部标签。即使它排在第一位,调用时也要写标签:

实例

import Foundation

// name 有默认值,自动获得外部标签 name
func greet(name: String = "Runoob") -> String {
    return "Hello, \(name)!"
}

print(greet())
print(greet(name: "RUNOOB"))

以上代码执行输出结果为:

Hello, Runoob!
Hello, RUNOOB!

可变参数

在参数类型后加三个点 ...,表示这个参数可以接收任意个同类型的值。

可变参数在函数体内是一个数组,可以不传,此时数组为空。

实例

import Foundation

// 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,调用时在变量前加 &

实例

import Foundation

// 两个参数都加 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

函数类型可以像普通类型一样用来声明变量。

实例

import Foundation

// 声明一个函数类型变量:接收两个 Int,返回 Int
let mathFunction: (Int, Int) -> Int = { $0 + $1 }

print(mathFunction(3, 4))

以上代码执行输出结果为:

7

返回类型为 Void 的函数,类型写作 () -> Void,也可以省略箭头写成 () -> ()


函数作为参数与返回值

函数类型既然是类型,自然可以当参数传进去,也可以当结果返回。

作为参数

调用者把「怎么做」交给函数,函数只负责在合适的时机调用它。

实例

import Foundation

// operation 是一个函数类型的参数
func apply(_ value: Int, using operation: (Int) -> Int) -> Int {
    return operation(value)
}

// 传入一个把参数平方的闭包
print(apply(5, using: { $0 * $0 }))

以上代码执行输出结果为:

25

作为返回值

返回一个函数,可以让调用方在稍后决定具体用哪种运算。

实例

import Foundation

// 返回类型是 (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

嵌套函数

函数可以定义在另一个函数内部,这种函数叫嵌套函数。

嵌套函数默认只在外部函数内部可见,常用来把辅助逻辑收拢在一起。

实例

import Foundation

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

实例

import Foundation

// 用类来保存闭包,避免全局可变状态
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 会自动把传入的表达式包装成一个闭包,调用处因此可以少写一层大括号。

它的价值在于延迟求值:只有真正用到时,表达式才会被计算。

实例

import Foundation

// @autoclosure 把表达式自动包装成闭包
func logIfTrue(_ condition: @autoclosure () -> Bool) {
    if condition() {
        print("条件为真")
    }
}

// 调用时直接写表达式,不需要写成 { 1 < 2 }
logIfTrue(1 < 2)

以上代码执行输出结果为:

条件为真

标准库里的 &&|| 就使用了这个特性,所以右边表达式只有在需要时才求值,短路效果才成立。


@discardableResult

函数有返回值时,调用却不接收,编译器会给出警告。

确实不需要返回值的地方,用 @discardableResult 标注函数,警告就消失了。

实例

import Foundation

// @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 中需要延迟求值时才用,日常代码里直接写闭包更直观。