Swift 3.0 从 ++ 的实现到 inout 和 defer 的小细节

发布于:2017-02-03 15:48,阅读数:2611,点赞数:17


>本文是一个在 Swift 3.0 中自加和自减的实现
>
>项目在我的[「Playground」](http://enumsblog.com/playground?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」](http://enumsblog.com/playground?sample=enum_posts_17004_1)中开源。几句简单的代码基本能够代替C语言的自加自减功能了。

Swift 中测试结果为:

![](//cdn.yuusann.com/img/posts/17004_1.png)

对于最后一个比较复杂的表达式,可以看出 Swift 跟C语言一样也是从右往左计算的,计算结果跟C语言中的是一致的:

![](//cdn.yuusann.com/img/posts/17004_2.png)

## 结语

其实 Swift 3.0 去除自加和自减也是有原因的,在非必要的情况下还是尽量使用推荐的写法为好,这习惯确实在对代码可读性没什么好处。养成良好编码习惯从我做起,这段代码也仅供特殊情况下使用。


评论:0条


返回列表

返回归档

返回主页