Swift 3.0 从 ++ 的实现到 inout 和 defer 的小细节
发布于:2017-02-03 15:48,阅读数:3243,点赞数:17
>本文是一个在 Swift 3.0 中自加和自减的实现 > >项目在我的[「Playground」(已下线)](about:blank?sample=enum_posts_17004_1)中开源 ## 引言 Swift 3.0 中删去了原 C Style 的自加和自减写法,转而推荐使用`+=`和`-=`写法。有时我们会在数组下标处直接修改某些计数值,而`+=`写法是个表达式,本身是不返回值的,因此无法代替原来的功能。 本文将通过重载运算符来找回自加和自减,顺便提及一下`inout`和`defer`的小知识。 ## 正文 自加和自减分别包含了两个运算符,共四个运算符。即:++前置/后置、--前置/后置。 前置运算符很简单,因为是先自加,再返回自加后的值: ```swift @discardableResult prefix func ++(x: inout Int) -> Int { x += 1 return x } ``` 测试时直接前置自加是可以实现功能的,这里会打印出`2`,即数组第二个元素: ```swift var i = 0 let list = [1, 2, 3, 4] print(list[++i]) ``` 但后置运算符不同,是先使用本身的值,使用完以后再进行自加,因此它似乎应该长这样: ```swift @discardableResult postfix func ++(x: inout Int) -> Int { let y = x x += 1 return y } ``` 但事实上,可以使用`defer`关键字推迟加法运算,可以写成这样: ```swift @discardableResult postfix func ++(x: inout Int) -> Int { defer { x += 1 } return x } ``` `defer`代码块会被压入栈中,待函数结束时弹出栈运行。 代码看上去怪怪的,这样真的没问题吗?`defer`和`return`哪个会被先执行呢?x 是`inout`的,不论是哪个先被执行,结果返回的不都是应该返回已经 +1 的 x 吗? 其实这里包含了两个问题,一是`inout`究竟怎样工作的,二是`defer`究竟是怎样工作的。 #### inout `inout`在写法上与C语言传递地址的写法十分类似,在调用函数传入参数是带有前缀`&`,就好像取地址传进去了一样,实则不然。 Swift 中 struct 是值类型的。 何为值,值是不能改变的,是`immutable`的,任何对值的修改其实就是新构造了一个来替换原来的。这里的`inout`也是如此,并不是传了地址进来,而是在这里构造了一个新的结构体,当函数结束时会复制回去替换原来的结构体。 当然,这个复制**并不一定**会真的复制。Swift 的`copy on write`也会分情况,当值类型的引用只有一个时是不会复制的,这段在猫神的书里有提到。 #### defer 这里`defer`代码块会被压入栈中,函数结束时执行。到底啥时候执行呢?是在`return`后执行,如果`return`调用了其他函数,这个函数会在`defer`代码块执行之前被执行。这个简单测试一下即可,也可以直接看汇编,上面代码的汇编长这样: ```c SelfPlusDemo`++ postfix(inout Int) -> Int: 0x100001c60 <+0>: pushq %rbp 0x100001c61 <+1>: movq %rsp, %rbp 0x100001c64 <+4>: subq $0x10, %rsp 0x100001c68 <+8>: movq %rdi, -0x8(%rbp) 0x100001c6c <+12>: movq (%rdi), %rax 0x100001c6f <+15>: movq %rax, -0x10(%rbp) //在这里调用了defer代码块,首地址为0x100001c90 0x100001c73 <+19>: callq 0x100001c90 ; SelfPlusDemo.(++ postfix (inout Swift.Int) -> Swift.Int).($defer #1) () -> () at main.swift 0x100001c78 <+24>: movq -0x10(%rbp), %rax 0x100001c7c <+28>: addq $0x10, %rsp 0x100001c80 <+32>: popq %rbp 0x100001c81 <+33>: retq ``` 回到上面自加的代码中。 这里`defer`代码块会被压入栈中,return 时还没有被执行,因此这里的值是正是我们想要的自加以前的值。按照`inout`的原理,这个 x 指向的不是函数外面的 x,这只是个临时变量,在`defer`代码块执时值被修改了也不会影响返回值,因为至始至终都是值,不是引用,在函数结束之后会把这个临时变量拷贝回去,改变函数外的 x 值。 因此整个流程都是在操作和复制值,都是`immutable`的,不存在互相影响的可能,因此这段代码能像想象中那样工作。 按照这个思路完成自减代码即可。完整代码在我的[「Playground」(已下线)](about:blank?sample=enum_posts_17004_1)中开源。几句简单的代码基本能够代替C语言的自加自减功能了。 Swift 中测试结果为:  对于最后一个比较复杂的表达式,可以看出 Swift 跟C语言一样也是从右往左计算的,计算结果跟C语言中的是一致的:  ## 结语 其实 Swift 3.0 去除自加和自减也是有原因的,在非必要的情况下还是尽量使用推荐的写法为好,这习惯确实在对代码可读性没什么好处。养成良好编码习惯从我做起,这段代码也仅供特殊情况下使用。
评论:0条