内存安全

默认情况下,Swift会阻止代码里的不安全行为。例如,变量在使用前完成初始化,在内存回收后无法被访问,并且数组的索引会做越界检查。

因为Swift管理内存,大部分时间 并不需要考虑内存访问的事情。然而,理解潜在冲突也是很重要的,可以避免写出访问冲突的代码。如果代码确实存在冲突,那在编译时或者运行时会得到错误。

理解内存访问冲突

内存的访问,会发生在你给变量赋值,或者传递参数给函数时。
而内存的访问冲突发生在代码多个访问同时访问同一个内存地址,造成的不可预计或者不一致的行为。

内存访问性质

内存访问冲突会发生在以下两个访问符合情况时:

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

内存的访问时长,要么是瞬时的,要么是长期的。主要区别在于,别的代码有没有可能在访问期间同时访问,也就是时间线上的重叠。一个长期访问可以被别的长期或者瞬时访问重叠

重叠访问主要出现在使用in-out参数的函数或方法 或者结构体的mutating方法里。

In-Out参数的访问冲突

一个函数会对它所有的in-out参数进行长期写访问。in-out参数的写访问会在所有非in-out参数处理完成后开始,直到函数执行完毕位置。如果有多个in-out参数,则写访问开始的顺序和参数顺序一致。

  1. 不能在访问以in-out形式传入后的原变量,即使作用域原则和访问权限允许--任何访问原变量的行为都会造成冲突

    解决方案是 显示的拷贝一份stepSize

    // 显式拷贝
    var copyOfStepSize = stepSize
    increment(&copyOfStepSize)
    // 更新原来的值
    stepSize = copyOfStepSize
    // stepSize 现在的值是 2
  2. 当向同一函数的多个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 访问冲突

    将 playerOneScore 作为参数同时传入就会产生冲突,因为它会发起两个写访问,同时访 问同一个的存储地址

注意:

因为操作符也是函数,它们也会对in-out参数进行长期访问

方法里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)  // 正常

但是当如果

oscar.shareHealth(with: &oscar)
// 错误:oscar 访问冲突

因为 mutating方法在调用期间需要对self发起访问,而同时in-out参数也需要写访问。在方法里,selfteammate指向统一存储地址。对同一块内存进行访问,并且重叠,就会产生冲突。

属性的访问冲突

结构体、元组和枚举都是值类型,由多个独立的值构成,所以修改值的一部分都是对于整个值的修改,意味着其中一个属性的读和写访问都需要访问整一个值。

  1. 对元组元素的写访问重写会产生冲突

    var playerInformation = (health: 10, energy: 20)
    balance(&playerInformation.health, &playerInformation.energy)
    // 错误:playerInformation 的属性访问冲突

    所以,在任何情况下,对于元组元素的写访问都需要对整个元组发起写访问。

  2. 对存储在全局变量的结构体属性写访问重叠

    var holly = Player(name: "Holly", health: 10, energy: 10)
    balance(&holly.health, &holly.energy)  // 错误
    

    大多数对于结构体属性的访问都会安全的重叠。例如,将上面例子的变量改为本地变量而非全局变量,编译器即可保证其是安全的

    func someFunction() {
        var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy) // 正常
    }

    此时编译器可以保证内存安全,因为两个存储尚需经任何情况下都不会互相影响。

限制结构体属性的重叠访问对于保证内存安全不是必要的。保证内存安全是必要的,但因为访问独占权的要求比内存安全还要更严格——意味着即使有些代码违反了访问独占权的原则,也是内存安全的,所以如果编译器可以保证这种非专属的访问是安全的,那 Swift 就会允许这种行为的代码运行。特别是当你遵循下面的原则时,它可以保证结构体属性的重叠访问是安全的:

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