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

函数的定义

//无参函数
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键+点击函数名称调出。swift

image.png

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


参数标签

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

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

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必须传值之外,其他带默认值的参数很随便,传不传都行,并且没有特别的顺序要求ide

你可能不理解我前面说的这个顺序要求是什么意思。若是你接触过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)oop

可是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函数的整理。

相关文章
相关标签/搜索