Swift

内存安全

本文详细介绍了 Swift 中的内存安全机制,重点讲解了内存访问冲突(Conflicting Access to Memory)的产生原因、典型场景(如 In-Out 参数、结构体属性访问)及其避免方法。

Swift 默认保证代码的内存安全。它会自动处理内存管理(通过 ARC),强制变量初始化,并检查数组越界。然而,在某些特定情况下,你需要理解并避免内存访问冲突(Conflicting Access to Memory)。

内存访问冲突

当两处代码同时访问同一块内存,且至少有一处是写操作时,就会发生内存访问冲突。这通常发生在多线程环境中,但在单线程中也可能发生(例如,在函数调用期间访问同一变量)。

冲突发生的三个条件(必须同时满足):

  1. 至少有一个是访问。
  2. 它们访问的是同一块内存。
  3. 它们的访问时间重叠

In-Out 参数的访问冲突

函数的 In-Out 参数在函数执行期间具有长期的写访问权限。如果你在调用函数时,将同一个变量同时作为多个 In-Out 参数传递,或者在函数内部访问该变量,就会产生冲突。

var stepSize = 1

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

increment(&stepSize)
// 错误:stepSize 的读访问与 increment(_:) 对 stepSize 的写访问冲突

解决办法是显式地拷贝一份副本:

var copyOfStepSize = stepSize
increment(&copyOfStepSize)
stepSize = copyOfStepSize
// 正确

方法中 self 的访问冲突

在结构体的变异方法(mutating method)中,self 具有对整个实例的写访问权限。如果你将 self 的某个属性作为 In-Out 参数传递给另一个函数,就会产生冲突。

struct Player {
    var name: String
    var health: Int
    var energy: Int

    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 在 shareHealth 中被写访问(self),同时作为参数被写访问(teammate)

属性的访问冲突

对于值类型(如结构体、元组),修改其任何一部分(如属性、元素)都需要对整个值进行写访问。这意味着你不能同时对同一个结构体的不同属性进行重叠的写访问。

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

注意:如果上述代码是在局部作用域(如函数内部)中,Swift 编译器可以证明这种访问是安全的(因为局部变量不会被其他线程访问),从而允许这种操作。但在全局作用域或类属性中,这是被禁止的。

总结

  • Swift 能够静态检测大多数单线程内存冲突。
  • 避免将同一变量同时作为多个 In-Out 参数传递。
  • 避免在变异方法中将 self 作为 In-Out 参数传递。
  • 理解值类型的整体突变特性。
在 GitHub 上编辑

上次更新于