Swift文档Chapter 24 内存安全


理解内存访问冲突

内存访问的冲突会发生在你的代码尝试同时访问同一个存储地址的时侯。同一个存储地址的多个访问同时发生会造成不可预计或不一致的行为。

内存访问性质

内存访问冲突时,要考虑内存访问上下文中的这三个性质:访问是读还是写,访问的时长,以及被访问的存储地址。特别是,冲突会发生在当你有两个访问符合下列的情况:

  • 至少有一个是写访问;
  • 它们访问的是同一个存储地址;
  • 它们的访问在时间线上部分重叠。

如果一个访问不可能在其访问期间被其它代码访问,那么就是一个瞬时访问。大多数内存访问都是瞬时的。瞬时访问和长期访问的区别在于别的代码有没有可能在访问期间同时访问,也就是在时间线上的重叠。一个长期访问可以被别的长期访问或瞬时访问重叠。

In-Out参数的访问冲突

一个函数会对它所有的in-out参数进行长期写访问。in-out参数的写访问会在所有非in-out参数处理完之后开始,直到函数执行完毕为止。
长期访问的存在会造成一个结果,你不能在访问以in-out形式传入后的原变量,即使作用域原则和访问权限允许——任何访问原变量的行为都会造成冲突。

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// 错误:stepSize 访问冲突


长期写访问的存在还会造成另一种结果,往同一个函数的多个in-out参数里传入同一个变量也会产生冲突:

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // 正常
balance(&playerOneScore, &playerOneScore)
// 错误:playerOneScore 访问冲突

方法里self的访问冲突

一个结构体的mutating方法会在调用期间对self进行写访问。

extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}

var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)  // 正常

如果和Maria分享血量,那么不会出现问题。

但是如果自己和自己分享血量,就会产生冲突。

同时写到了同一个地方。

属性的访问冲突

结构体,元组和枚举的类型都是由多个独立的值组成的,例如结构体的属性或元组的元素。因为它们都是值类型,修改值的任何一部分都是对于整个值的修改,意味着其中一个属性的读或写访问都需要访问整一个值。
在实践中,大多数对于结构体属性的访问都会安全的重叠。遵循下面的原则时,它可以保证结构体属性的重叠访问是安全的:

  • 你访问的是实例的存储属性,而不是计算属性或类的属性;
  • 结构体是本地变量的值,而非全局变量;
  • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了。