Swift 中的函数盘点
Swift中的函数盘点
- 「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
前言
Swift
已经被越来越多的公司使用起来,因此Swift
的学习也应该提上日程了。本篇就先探索Swift
中的函数,主要包括以下几个方面:
- Swift函数定义
- Swift函数参数与返回值
- Swift函数重载
- 内敛函数优化
- 函数类型、嵌套函数
一、Swift函数定义
函数的定义包含函数名、函数体、参数及返回值,定义了函数会做什么、接收什么以及返回什么。函数名前要加上 func
关键字修饰。如下为一个完整的函数定义事例:
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
- 函数名: greet
- 参数:圆括号中(person: String)即为参数,person为参数名,String为类型
- 返回值:使用一个 -> 来明确函数的返回值,在该事例中定义了一个 String类型的返回值
二、函数返回值与参数
2.1 函数返回值
从返回值的角度看,函数可以分为有返回值和无返回值两种。无返回值的函数可以有如下三种定义方式:
func testA() -> Void {
}
func testB() -> () {
}
func testC() {
}
let a = testA()
let b = testB()
let c = testC()
打印 a、b、c 可以发现,三者的类型均为()
,即空元组。在 Void 的定义处也可以发现,Swift中 Void 就是空元组。也就是说上面三种方式是等价的,都表示无返回值的情况,不过从代码简洁程度上来说,最后一种更方便使用。
还有一种函数有返回值的情况,如同第一节中所述的函数定义方式,即为一种返回值为String
的函数。在Swift
中,函数的返回值可以隐式返回,如果函数体中只有一句返回代码,则可以省略return
关键字。如下代码所示,两种写法是等价的:
func testD() -> String {
return "正常返回"
}
func testE() -> String {
"隐式返回"
}
Swift
中还可以通过元组实现多个返回值的情况,如下所示:
func compute(a:Int, b: Int) -> (sum: Int, difference: Int) {
return (a+b, a-b);
}
compute函数返回一个元组,包含了求和与求差,实现了返回多个值的情况。
2.2 函数参数
与OC不同的是,Swift
中函数的参数是let
修饰的,参数值是不支持修改的。如下图所示,可以证明。
2.2.1 函数标签
Swift
的函数参数除了形参外,还包含一个参数标签。形参在函数内部使用,使得函数体中使用没有歧义,而函数标签用于在函数调用时使用,其目的是增加可读性。函数标签是可以省略的,使用_
表示即可,需要注意的是,_
与不设置函数标签是不一样的,如下图所示:当使用_
时,调用函数不会显示函数标签,而不设置函数标签会把形参作为函数标签。
2.2.2 函数默认参数值
Swift
可以给函数参数设置默认值,设置了默认值的参数,在函数调用时可以不传参由于参数 a 有了默认值 8,所以在调用时只传参 b 就可以。同样的,如果参数均有默认值,则在调用函数时,都可以不传值。如图所示,由于两个参数均有默认值,在调用时都不传值,就像调用了一个无参函数一样。
Swift
中设置函数参数默认值可以不按照顺序,因为Swift
中有函数标签,不会造成歧义。而在C++
中,则必须要按照从右往左的顺序依次设置,两者对比如下:下面一张图是C++
的调用,没有按照顺序设置默认值,直接报错缺失b的默认值,而Swift
中则不会。但是,如果Swift
函数参数都隐藏了函数标签,则无法识别是给哪个参数,只能按照从右往左的方向赋值,这样就会照成报错,如下图所示:在调用函数时,直接报错缺失第二个参数。因此,在Swift
中,如果省略了函数参数标签,要保证所有的函数参数都有值,或者都可以得到赋值。
2.2.3 可变参数
与OC的NSLog
参数一样,Swift
函数也提供了可变参数,其定义方式是 参数名:类型...
,可以参照系统的print
函数定义:print
函数的第一个参数即为可变参数,参数类型为Any,可以接受任意类型,输入时以,
分割即可。
可变参数需要注意的一点是,在紧随其后的一个参数不能省略参数标签,如下图所示:
参数b
也是一个Any类型,如果省略了参数标签,则在调用函数时就没有了标签区分,仅凭,
编译器无法确定该将参数赋值给item还是b,因此会报错。
可变参数本质上是一个数组,可以在函数内部使用参数,查看其类型如下:可以看到 item 实际上是一个 Any 类型的数组。
2.2.4 inout修饰的参数
在OC和C中,我们可以通过指针传参,以达到在函数内部修改函数外部实参的值的目的。在Swift
中,也提供了类似的方法,不过需要使用inout修饰一下参数,具体使用方式如下:
number的值本来为10,经过inoutFunc函数调用,结果变为了20
。那么 inout 是如何改变了外部实参的值的呢?有种说法是与OC一样,采用了指针传值
的方式改变;还有说法是 inout
在底层是一个函数,将其修饰的函数内部的值通过这个函数重新赋值外部实参。针对这两种说法,我们可以通过汇编来验证下,本次使用的是真机调试,因此使用的是ARM下的汇编。
将上图中12行
和22行
的断点打开,并打开XCode的汇编调试 Debug -> Debug Workflow -> Always show Disassembly
。运行工程,首先进入22行的断点:图中红框处为 inoutFunc 函数的调用处,在上面28行可以发现一行代码 ldr x0, [sp, #0x10]
,这句代码的意思是,将[sp, #0x10]的值赋值给 x0 寄存器,[sp, #0x10]表示 sp+#0x10
的地址,也就是说 x0 寄存器现在存储的是一个地址,通过 register read x0
命令可知改地址为 x0 = 0x000000016dbf9a80
。
单步调试进入 inoutFunc
函数,得到如下代码:
执行到第4行,再次读取 x0 寄存器得到了相同的值x0 = 0x000000016dbf9a80
,此时通过 x/4gx 读取内存地址0x000000016dbf9a80
的值,得到结果如下:
红框中的值 0x000000000000000a
换算成十进制正是 10
。走到第6行汇编代码,将x0
存储的地址所指向的内容存到x8
寄存器,然后将值加10,就此完成对外部实参值的改变。在viewDidLoad
中调用inoutFunc
后并没有对于number
的重新赋值,也证实了inout
是通过地址传递改变外部实参的值。
使用inout
需要注意两点:
- 1、inout只能传入可以被多次赋值的,即不能传入常量和字面量
- 2、inout不能修饰可变参数
三、函数重载
函数重载指的是函数名相同,但是参数名称不同 || 参数类型不同 || 参数个数不同 || 参数标签不同。需要注意的是,函数重载(overload)与函数重写(override)是两个概念,函数重写涉及到继承关系,而函数重载不涉及继承关系。另外,在OC中没有函数或方法的重载,只有重写。以下是几个函数重载的例子:
可以看到,四个函数的方法名称相同,但是参数不同,实际上并不会报错,这就是方法重载。
不过方法重载也有需要注意的地方:
- 方法重载与函数返回值无关,即函数名及参数完全相同的情况下,如果返回值不同,不构成函数重载,编译器会报错。
如图所示,在调用方法时,编译器不知道该调用哪个函数,因此会报二义性错误。
- 方法重载与默认参数值的情况
从图中可以发现,由于第二个函数给参数c设置了默认值,在调用时形式上与第一个函数一样,不过编译器在此并不会报错,猜想是因为第二个函数还有一种test(a: , b: , c: )
的调用形式。
四、inline内联函数
内联函数,其实是指开启了编译器内联优化后,编译器会将某些函数优化处理,该优化会将函数体抽离出来直接调用,而不会给这个函数再开辟栈空间。
func test() {
print("test123")
}
test()
复制代码
如以上函数所示,调用test()
时,需要为其开辟栈空间,而其内部只调用了一个print
函数,所以在开启内联优化的情况下,可能会直接调用print
函数。
开启内联优化的方式如下图:Debug
模式下默认不开启优化,Release
模式下默认是开启的。为了测试内联优化的现象,这里先将Debug
模式开启优化,之后在test()调用处打断点,再运行工程会发现,直接打印了test123
,然后在test
函数内部打断点,进入汇编如下:全局搜索发现没有test
函数的调用,而是直接调用了print
函数。
不过内联优化,也不是对所有函数都会进行优化,以下几点不会优化:
- 函数体代码比较多
- 函数存在递归调用
- 函数包含动态派发,例如类与子类的多态调用
内联函数还有内联参数控制@inline(never)
和 @inline(__always)
- 使用
@inline(never)
修饰,即使开启了编译器优化,也不会内联 - 使用
@inline(__always)
修饰,开启编译器优化后,即使函数体代码很长也会内联,但是递归和动态派发依然不会优化
五、函数类型
每一个函数都可以符合一种函数类型,例如:
func test() {
print("test123")
}
对应 () -> ()
func compute(a:Int = 8, b: Int = 9) -> Int {
return a+b;
}
对应 (Int, Int) -> Int
复制代码
上述代码中,() -> ()
和 (Int, Int) -> Int
都表示一种函数类型。可以发现函数类型是不需要参数名的,直接标明参数类型即可。
函数类型也可以用作函数的参数和返回值,使用函数类型作为返回值的函数被称为高阶函数,例如:
// 函数类型作为参数
func testFunc(action:(Int) -> Int) {
var result = action(2)
print(result)
}
func action(a:Int) -> Int {
return a
}
testFunc(action: action(a:))
// 函数类型作为返回值
func action(a:Int) -> Int {
return a
}
func testFunc() -> (Int) -> Int {
return action(a:)
}
let fu = testFunc()
print(fu(3))
复制代码
六、嵌套函数
Swift中,可以在函数内部定义函数,被称为嵌套函数,如下代码所示:
func forward(_ forward: Bool) -> (Int) -> Int {
func next(_ input: Int) -> Int {
input + 1
}
func previous(_ input: Int) -> Int {
input - 1
}
return forward ? next : previous
}
复制代码
像上面这样在函数内部定义其他的函数,其目的是为了将函数内部的实现封装起来,外部只看到调用了 forward,而不需要知道其内部的实现逻辑,当然也不能直接调用内部的嵌套函数。
总结
相对于OC,Swift中主要增加了以下几点:
- 参数标签
- 函数重载
- 嵌套函数
整体而言,个人感觉Swift的函数使用起来更加方便,参数标签使得代码可读性更强。以上即为本篇关于Swift函数的总结,如有不足之处,欢迎大家指正。