令人迷惑的Swift行为
一、引言
所有的程序员都知道,多线程可能导致程序发生逻辑错误。此类问题的一般原因是,对相同内存的访问导致脏数据。现在,语言、工具、库等的不断改善,大幅降低多线程导致的问题的可能性。如,在OC当中,GCD的出现,大幅降低了多线程操作的难度。但是,多线程问题依然会不时发生。
虽然,这类问题不时发生,也不一定好排查、修改。但是,从理论角度上来说,我们对此种问题原理、本质是理解比较清楚的。
但是,在Swift之中存在类似的问题,此类问题令人迷惑的行为。这种行为与上面所说的多线程导致脏数据的问题结果相同,但是又不像多线程问题那样让人明晰其背后的原理。
二、废多看码
// 这是一个函数,其有两个输入输出参数
// 此函数的功能也较简单:把输入参数求和后,把参数修改为和的一半
// 如果不明白inout是什么意思,请自行百度,或者查看笔者的相关swift语法文章
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
// 这里定义了一个元组
var playerInformation = (health: 10, energy: 20)
// 期望此行代码执行之后结果是
// playerInformation.health=15
// playerInformation.energy=15
balance(&playerInformation.health, &playerInformation.energy)
请诸位读者先不要往下看,此时以自己的理解,来看看上面的代码是否有问题。
以笔者对C、C++、OC等语言的理解,以及对Swift的理解,认为以上代码是没有什么问题的。但是,世事难料,调用balance函数的那一行语句居然导致运行时崩溃!!!
i报错截图
- 右侧绿色框所标识的编译器的报错信息:同时访问地址0x0000000100002068,但是写访问需要是独占的。
- 再看左侧两个红框所标识出来的,对地址0x0000000100002068的修改,他们都指向了调用balance的语句。
- 0x0000000100002068就是playerInformation变量的地址
三、追根溯源
上述代码抛出了一个令人迷惑的Swift行为。此问题是由Swift对于内存的访问控制策略导致的。
3.1 Swift内存访问控制
3.1.1 问题发生的前提
- 读写(至少有一个写内存)内存
- 涉及同一块内存
- 读写同时发生
这里的同时,不是指多线程的同时(此种同时已经为大家所熟知),而是指单线程!
3.1.2 问题发生的表现
- 运行时崩溃(如“废多看码”章节所示)
-
编译期错误(如下图所示)
image.png
3.1.2 内存问题出现的场景
- 调用含有inout参数的方法(从进入方法一直到方法结束,都会对参数进行访问)
- 调用有mutating修饰的方法(从进入方法一直到方法结束,都会隐含对self进行访问)
以上方法操作的是值类型(结构体、元组)。
非值类型不会出现问题 值类型出现问题
对于值类型来说,访问其中的一个成员就会导致对整个实例的独占访问。
3.2 “废多看码”章节的解释
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerInformation = (health: 10, energy: 20)
// 此语句输出playerInformation的地址
withUnsafePointer(to: &playerInformation) {ptr in print(ptr)}
balance(&playerInformation.health, &playerInformation.energy)
对于上述代码,对balance函数的调用,我们进行分析。
- 含有inout参数
- 针对第一个参数,需要对playerInformation进行独占访问
- 针对第二个参数,需要对playerInformation进行独占访问
- 因为同时需要对playerInformation进行独占访问,所以出问题。
- 满足了:同时(在balance函数内),访问同一块内存(playerInformation所代表的内存),且有写操作(其实两个都是对playerInformation进行写操作)
也许从读者角度来看,两次对playerInformation的独占访问,不应该出问题,但是Swift的编译器不这么看。
3.3 未完待续
看了以上内容之后,不知道读者是明白了呢,还是更糊涂了,还是有些明白了。笔者看了之后,对这方面有了一些了解,所以就写了这篇文章。虽然笔者知道这是Swift编译器在背后做的一些工作,但是依然不是很理解这么做的原理。
除了上述例子外,其实还有更令人疑惑的一些内容。
-
当变量是全局变量时会出错
全局变量出错 -
当变量是局部变量不会出错
局部变量不会出错
从应用角度来看,以上的理解已经可以了,碰到问题时不以至于十分惊讶。但是对于编译器在背后捣的鬼,还是希望能够有深入理解。如果哪位读者理解了,请不吝赐教!
如果读者想看更多的资料,可以查看内存安全。