内存访问的三个性质:读还是写,访问的内存地址,访问的时长
只要包含以下情况中的任意两种,都会造成 访问冲突:
- 至少有一个是写访问
- 它们访问的是同一个存储地址
- 它们的访问在时间线上部分重叠
- 重叠的访问主要出现在使用 in-out 参数的函数和方法或者结构体的 mutating 方法里
In-Out 参数的访问冲突
一个函数会对它所有的 in-out 参数进行长期写访问。in-out 参数的写访问会在所有非 in-out 参数处理完之后开始,直到函数执行完毕为止。如果有多个 in-out 参数,则写访问开始的顺序与参数的顺序一致。
因为操作符也是函数,它们也会对 in-out 参数进行长期访问。例如,假设
changeTwo(_:_:)
是一个名为<^>
的操作符函数,那么num1 <^> num1
也会造成像changeTwo(&num1, &num1)
一样的冲突。
var num1 = 1
var num2 = 2
func changeNum(with num:inout Int)->Int {
return num + num1
}
print(changeNum(with: &num1)) // 错误:changeNum 内部直接访问了 num1
func changeTwo(num1:inout Int,num2:inout Int)->Int{
return num1 + num2
}
changeTwo(num1: &num1, num2: &num1) // 错误:包含写访问,访问同一个内存地址,访问时间线重叠
方法里 Self 的访问冲突
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth // 访问的不是实例属性,没问题
}
}
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
func balance(_ fromHealth:inout Int,_ toHealth:inout Int) {
fromHealth = 1
toHealth = 2
print(fromHealth,toHealth)
}
var player = Player.init(name: "Li", health: 11, energy: 8)
//player.shareHealth(with: &player)// 错误:shareHealth内隐式访问self,会造成访问同一个内存地址
var player2 = Player.init(name: "Ming", health: 15, energy: 8)
player.shareHealth(with: &player2) // 正确
属性的访问冲突
var player = Player.init(name: "Li", health: 11, energy: 8)
balance(&player.health, &player.energy) // 运行错误: 值类型,修改值的任何一部分都是对于整个值的修改,意味着其中一个属性的读或写访问都需要访问整一个值
func someFunction (){
var player2 = Player.init(name: "Ming", health: 15, energy: 8)
balance(&player2.health, &player2.energy) // 运行正常:改为本地变量而非全局变量,编译器就会可以保证这个重叠访问是安全的
限制结构体属性的重叠访问对于保证内存安全不是必要的。保证内存安全是必要的,但因为访问独占权的要求比内存安全还要更严格——意味着即使有些代码违反了访问独占权的原则,也是内存安全的,所以如果编译器可以保证这种非专属的访问是安全的,那 Swift 就会允许这种行为的代码运行。特别是当你遵循下面的原则时,它可以保证结构体属性的重叠访问是安全的:
- 你访问的是实例的存储属性,而不是计算属性或类的属性
- 结构体是本地变量的值,而非全局变量
- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获了
如果编译器无法保证访问的安全性,它就不会允许那次访问。
网友评论