Swift底层原理探索1----函数

1,613 阅读13分钟

函数的定义

//无参函数
func pi() -> Double {
    return 3.14
}
//带参函数
func sum(v1: Int, v2: Int) -> Int { // 形参默认是let, 并且只能是let,所以不用纠结let or  var
    return v1 + v2
}
//函数的调用
sum(v1: 10, v2: 20)

//无返回值  下面三种等价
func sayHello() -> Void {
    print("Hello")
}
func sayHello2() -> () {
    print("Hello")
}
func sayHello3() {
    print("Hello")
}

隐式返回

如果整个函数体是一个单一表达式,那么函数会隐式(自动)返回这个表达式

//隐式返回
func sum2(v1: Int, v2: Int) -> Int { // 函数体内只有一条语句,便可以不用写return, 如果有多条语句,则必须通过 return关键字来返回
     v1 + v2
}
//函数的调用
sum2(v1: 10, v2: 20)

返回元组:实现多返回值

//通过元祖实现多返回值,将多个返回值整理到一个元祖数据结构中进行一块返回
func calculate(v1: Int, v2: Int) -> (sum: Int, difference: Int, average: Int) {
    let sum = v1 + v2
    return (sum, v1 - v2, sum >> 1)
}

let result = calculate(v1: 20, v2: 10)
result.sum
result.difference
result.average

函数文档的注释

/// 求和【概述】
///
/// 将2个整数相加【更详细的描述】
///
/// - Parameter v1: 第一个整数
/// - Parameter v2: 第二个整数
/// - Returns: 2个整数的和
///
/// - Note: 传入两个整数即可【批注】
func sum(_ v1: Int, _ v2: Int) -> Int {
    v1 + v2
}

函数文档的注释需要严格按照上面的模版来填写。苹果官方非常建议我们对函数进行详细的文档注视,有助于提高代码的可读性。注释生成的效果如下,通过option键+点击函数名称调出。

image.png

更详细的关于函数文档注释请参考苹果官方提供的接口设计规范


参数标签

//函数定义里面,通过形参time的语义,很容易理解传进来参数的性质或者作用
func goToWork(at time: String) {
    print("This time is \(time)") 
}
//函数调用的时候,实际参数取代了time,
goToWork(at: "10:00")

上例中的goToWork函数,参数拥有两个标签attime,其中time作为为形参,在函数体内部实现中被用来传递参数,而at则是在函数调用的时候使用。 上面的示例可感觉到,通过参数标签,使得函数的定义和调用,都非常符合口语习惯,利用苹果的提供的这个特性,参照我们正常的语言习惯来合理地设置参数标签,可以很好地提升代码的可读性。这也符合了苹果的API设计准则。

func sum(_ v1: Int, _ v2: Int) -> Int {
    v1 + v2
}
sum(10, 20)

通过_来省略参数标签,可以使得函数的调用无需参数数名,让代码很精简。但是对于这一点的使用需要结合实际情况,不要为了精简代码而影响到代码的可读性,从而给后期的维护带来不便。


默认参数值

首先看一下带默认值函数的范例

func check(name: String = "nobody", age: Int, job: String = "none") {
    print("name=\(name), age=\(age), job=\(job)")
}
check(name: "Jack", age: 20, job: "Docter")
check(name: "Rose", age: 18)
check(age: 10, job: "Batman")
check(age: 15)

范例中可见,check的三个参数中,参数age是没有默认值的,而后的几种调用过程,可以得出的结论是,age必须传值以外,其余带默认值的参数很随便,传不传都行,而且没有特别的顺序要求

你可能不理解我前面说的这个顺序要求是什么意思。如果你接触过C++,那么就知道C++ 的函数也是可以给参数设置默认值的,但是要求必须从右向左依次设置,不能间隔。比如下面这个test的函数

void test1(int a, int b, int c = 10, int d = 20){
}//在参数列表中,从右向左分别给`d`和`c`设置了默认值,可以编译通过✔️

void test2(int a = 10, int b, int c, int d = 20){
}//在参数列表中,从右向左分别给`d`和`a`设置了默认值,中间跳过了`c`和`b`,无法编译通过✖️

不同于swiftC++ 并没有参数标签这种特性,那么C++ 就只能将实参按照传入顺序,在形参列表里面,从左向右进行赋值。那么test1(50, 60)就很好理解,加上原本带有默认值的参数,就等价于test1(50, 60, 10, 20)

但是test2(50, 60)就无法被计算机理解了,因为按照C++ 的解析规则,5060会分别赋值给参数ab,但是实际上我们是想赋值给参数bc,由于这里出现了二义性,因此test2在实际中是无法被使用的,其实编程语言的各种奇怪限制,很多就是为了消除代码的二义性,要知道其实计算机是很笨的。

回看swift的参数标签特性,因为调用函数需要带上参数标签,因此swift在解析的时候,可以根据参数标签,把实参和形参对应绑定起来。因此我们在给swift函数设置参数默认值的时候,可以不考虑顺序。但是,如果我们给函数里面的参数都加上_,效果上就相当于C++函数了,如下

void testCpp(int a, int b, int c , int d ){
}

等价于

func testSwift(_ a: Int, _ b: Int, _ c: Int, _ d: Int){
}

在这里插入图片描述 看的出来,虽然我们给testSwift部分参数设置了默人参数,但是因为设置顺序问题,导致实际上必须给所有参数传值,才能成功调用,也就是说默认参数无法起到应有的效果。如果参照C++ 的做法,从右向左依次设置默认值,则可以通过只传非默认值参数来进行函数调用,如上看的test


可变参数(Variadic Parameter)

func sum(_ numbers: Int...) -> Int {
    var total = 0
    for number in numbers {
        total += number
    }
    return total
}
sum(1,3,4,5,50,90)
sum(5,88,2)
  • 一个函数最多只能有1个可变参数
  • 紧跟在可变参数后面的参数不能省略参数标签,这么要求的目的很容易理解,就是为了借助可变参数之后的那个参数的参数标签来确定可变参数结束的位置。
//参数string不能省略标签
func test(_ numbers: Int..., string: String, _other: String) {
    
}
test(10,20,40, string: "jack", _other: "rose")

Swift自带的print函数

Swift自带的print函数就是一个可变参数运用的范例

/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
public func print_copy(_ items: Any..., separator: String = " ", terminator: String = "\n"){
    //系统实现
    }

输入输出参数(In-Out Parameter)

  • 可以用inout定义一个输入输出参数:可以在函数内部修改外部实参的值
  • 可变参数不能标记为inout
    1. inout参数的本质是地址传递(引用传递)
    2. inout参数不能有默认值
    3. inout参数只能传入可以被多次赋值的
func swapValue(_ v1: inout Int, _ v2: inout Int) {
    let tmp = v1
    v1 = v2
    v2 = tmp
}
var num1 = 10
var num2 = 20
swapValue(&num1, &num2)

func swapValue2(_ v1: inout Int, _ v2: inout Int) {
//利用元组来实现
    (v1, v2) = (v2, v1)
}

现在通过汇编手段,来研究一下inout的实现原理。在C语言里,我们通过&可以访问变量的地址,但在Swift里面,这个功能被屏蔽了,我们只有在传inout参数的时候,才可以使用&,其他地方使用会直接编译器报错。

首先我们需要新建一个命令行项目 image

准备如下测试代码

var number = 10
func test(_ num: inout Int) {
    num = 20
}
func test2(_ num: Int) {
}

test(&number)
test2(number)

加个断点,进入汇编 在这里插入图片描述 在这里插入图片描述 图中展示了test()test1()这两个函数的汇编吗,通过传递参数所使用的指令,我们就得到了结果

  • leaqtest函数用的这个指令是进行地址传递的,也就是说test函数接受了一个内存地址作为参数
  • movqtest2函数用的这个指令是进行复制操作的,作用就是将参数的值传入函数内部

根据上面的发现,说明了,inout参数被传入的实际上是外部变量的地址。这样我们就理解了为什么上面还要求inout参数不能有默认值,这里传入的是一个内存地址,默认值没有任何意义,并且刚才说了Swift不允许我们获取内存地址,因此其实没有手段可以将内存地址设定成默认值。


函数重载(Function Overload)

  • 规则
    1. 函数名相同
    2. 参数个数不同 || 参数类型不同 || 参数标签不同
func add(v1: Int, v2: Int) -> Int {
    v1 + v2
}
func add(v1: Int, v2: Int, v3: Int) -> Int {//参数个数不同
    v1 + v2 + v3
}
func add(v1: Int, v2: Double) -> Double {//参数类型不同
    Double(v1) + v2
}

func add(_ v1: Int, _ v2: Int) -> Int {//参数标签不同
    v1 + v2
}
func add(a: Int, b: Int) -> Int {//参数标签不同
    a + b
}

函数重载注意点

  • 返回值类型与函数重载无关
  • 默认参数值和函数重载一起使用产生二义性时,编译器并不会报错(在C++中是会报错的)
  • 可变参数、省略参数标签、函数重载一起使用产生二义性时,编译器有可能会报错

内联函数

  • 如果开启了编译器优化(Release模式会默认开始优化),编译器会自动将某些函数变成内联函数,将函数调用展开成函数体
  • 以下情况不会被自动内联
    1. 函数体比较长
    2. 包含递归调用的函数
    3. 包含动态派发的函数
    4. ......

@inline

Release模式下,编译器已经开启优化,会自动决定哪些函数比内联展开,因此没必要手动使用@inline将函数调用展开成函数体,看如下代码

func test() {
    print("test")
}
test()

当前的优化设置如下 image

说明当前Debug模式是没有优化的,我们看下汇编情况image 可以清晰看到,调用了test()函数,在进一步的验证,我们可以在test()内部加上断点 image 其汇编如下 image 可以看到print()打印语句确实是在test()函数内部执行的。我们调整一下Debug模式下的优化策略为Optimize for Speed在看下test()函数有没有被调用,情况如下 image

发现test()没有被调用,函数就结束了。但是print("test")语句却执行了,因此追踪一下print语句的执行环境

image image 可以看到,print语句是在main函数里被调用的,Swiftmain函数是会自动生成的,在这里就是我们当前代码所在的文件。因此说明经过优化,代码test(),并没有进行函数调用,而是将它内部的语句print("test")直接展开到当前的作用域进行执行,因为不用再调用test()函数,所以就节省了函数调用所需要的栈空间的开辟以及销毁等一些操作开销,从而提升了程序的运行速度。

Swift编译器设计得很巧妙,能自动判断哪些函数需要进行内联展开,哪些不需要。通常,以下几种情况不需要进行内联

(1) 内部语句较多的函数 image image 这里在编译器有优化的情况下,maintest()进行了调用,编译器并没有内联展开test()函数,这很容易理解,因为此时test()内部的语句太多,被内联展开之后会产生大量的代码(转换到底层就是大量的010101字节码),如果程序内部大量调用test()函数,那么对其内联展开的代价显然大过了进行正常调用的开销,因此编译器选择不对其进行内联展开

(2) 存在递归调用的函数,这个也很好理解,可想而知,如果要展开一个递归函数,那么也是一层套一层的循环展开,这个代价显然也是大于了对函数的直接调用

(3) 存在动态派发(动态绑定)的函数,这个跟上面两个情况不同,并不是不想,而是不能。因为对一个函数进行内联展开的前提,能在编译阶段就确定改函数内部要执行的代码是什么。如果是如下情况 image 可以看到,如上的多态情况下,在编译阶段是无法确定p.test()具体调用的是哪个类里面的test()。只能等到程序运行阶段才能决定,因此编译器的内联优化在这里无法实现。

以下再了解一下Swift给我们提供的一些内联的使用方法(如果需要的话),一般来说,大部分情况下我们基本都用不着@inline

//永远不会被内联(即使开启了编译器优化)
@inline(never) func test() {
    print("test")
}

//开启编译器优化后,即使代码很长,也会被内联(递归调用,动态派发函数出之外)
@inline(__always) func test1() {
    print("test")
}

函数类型(Function Type)

每一个函数都是有类型的,函数类型由形式参数类型返回值类型组成

func test2() {}  // () -> Void   or   () -> ()
func sum1(a: Int, b: Int) -> Int {
    a + b
} //(Int, Int) - Int

//定义变量
var fn: (Int, Int) -> Int = sum1
fn(2, 3) //调用时不需要参数标签

函数类型作为函数参数

func sum4(v1: Int, v2: Int) -> Int {
    v1 + v2
}
func difference(v1: Int, v2: Int) -> Int {
    v1 - v2
}
func printResult(_ mathFn: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFn(a, b))")
}
printResult(sum4, 5, 2) //Result: 7
printResult(difference, 5, 2) //Resule : 3

函数类型作为函数返回值

func next(_ input: Int) -> Int {
    input + 1
}
func previous(_ input: Int) -> Int {
    input - 1
}
func forward(_ forward: Bool) -> (Int) -> Int {
    forward ? next : previous
}
forward(true)(3) // 4
forward(false)(3) // 2

顺便了解一下,如上的forward函数就是所谓的高阶函数(将一个函数作为返回值的函数)


typealias(别名)

  • typealias用来给类起别名
//基本数据类型别名
typealias Byte = Int8
typealias Short = Int16
typealias Long = Int64
//元祖别名
typealias Date = (year: Int, month: Int, day: Int)
func test3(_ date: Date) {
    print(date.0)
    print(date.year)
}
test3((2020, 2, 29))
//函数别名
typealias IntFn = (Int, Int) -> Int
func difference1(v1: Int, v2: Int) -> Int {
    v1 - v2
}
let fn1: IntFn = difference1
fn1(20, 10)

func setFn(_ fn:IntFn) {}
setFn(difference1)
func getFn() -> IntFn {difference1}
  • 按照Swift标准库的定义,Void就是空元组()
public typealias Void = ()

嵌套函数(Nested Function)

func forward1(_ forward: Bool) -> (Int) -> Int {
    func next(_ input: Int) -> Int {
        input + 1
    }
    func previous(_ input: Int) -> Int {
        input - 1
    }
    return forward ? next : previous
}
forward1(true)(2)
forward1(false)(2)

如果有些函数的实现过程你不想暴露给别人,比如上面的next()previous()函数,那么可以通过上述的方法将他们隐藏在一个壳函数(forward())内部,并通过控制条件(_ forward: Bool)来调用。

已上就是关于Swift函数的整理。