- Swift 安全性
- 使用前就初始化
- 內(nèi)存在變量釋放后不能再訪問
- 數(shù)組會(huì)檢查越界錯(cuò)誤
- Swift 還通過要求標(biāo)記內(nèi)存位置來確保代碼對(duì)內(nèi)存有獨(dú)占訪問權(quán),以確保了同一內(nèi)存多訪問時(shí)不會(huì)沖突。
- 了解一下什么情況下會(huì)潛在導(dǎo)致沖突
- 避免寫出對(duì)內(nèi)存訪問沖突的代碼
理解內(nèi)存訪問沖突
- 出現(xiàn)場(chǎng)景:給變量賦值癞谒,或者傳遞參數(shù)給函數(shù)
- 比如說藤滥,下面代碼同時(shí)包含了讀取訪問和寫入訪問:
// 向 one 所在的內(nèi)存區(qū)域發(fā)起一次寫操作
var one = 1
// 向 one 所在的內(nèi)存區(qū)域發(fā)起一次讀操作
print("We're number \(one)!")
- 添加預(yù)算項(xiàng)進(jìn)入表里的時(shí)候鳖粟,它只是在一個(gè)臨時(shí)的,錯(cuò)誤的狀態(tài)拙绊,因?yàn)榭倲?shù)還沒有被更新
- 在添加數(shù)據(jù)的過程中讀取總數(shù)就會(huì)讀取到錯(cuò)誤的信息向图。
../_images/memory_shopping_2x.png
這里訪問沖突的討論是在單線程的情境下討論的,并沒有使用并發(fā)或者多線程标沪。
在單線程遇到內(nèi)存訪問沖突榄攀,Swift 會(huì)保證你在要么編譯時(shí)要么運(yùn)行時(shí)得到錯(cuò)誤。
對(duì)于多線程的代碼金句,可以使用 Thread Sanitizer 去幫助檢測(cè)多線程的沖突
內(nèi)存訪問性質(zhì)
- 沖突會(huì)在兩個(gè)訪問檩赢,同時(shí)滿足以下條件時(shí)發(fā)生:
- 至少一個(gè)是寫入訪問;
- 它們?cè)L問的是同一塊內(nèi)存违寞;
- 它們的訪問時(shí)間重疊贞瞒。
- 讀和寫訪問的區(qū)別
- 寫訪問會(huì)改變存儲(chǔ)地址,而讀操作不會(huì)(存儲(chǔ)地址是指向正在訪問的東西(例如一個(gè)變量趁曼,常量或者屬性)的位置的值)
- 內(nèi)存訪問的時(shí)長(zhǎng)要么是瞬時(shí)的军浆,要么是長(zhǎng)期的
- 瞬時(shí)訪問:一個(gè)訪問在啟動(dòng)后其他代碼不能執(zhí)行直到它結(jié)束后才能
- 兩個(gè)即時(shí)訪問不能同時(shí)發(fā)生
- 大多數(shù)內(nèi)存訪問都是即時(shí)
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// 打印“2”
- 長(zhǎng)期訪問:會(huì)在別的代碼執(zhí)行時(shí)持續(xù)進(jìn)行
- 長(zhǎng)期訪問,可被別的長(zhǎng)期訪問彰阴、訪問重疊
- 重疊訪問場(chǎng)景
- 使用 in-out 參數(shù)的函數(shù)和方法
- 結(jié)構(gòu)體的 mutating 方法里
In-Out 參數(shù)的訪問沖突
- 沖突本質(zhì):一個(gè)函數(shù)會(huì)對(duì)它所有的 in-out 參數(shù)進(jìn)行長(zhǎng)期寫訪問
- 順序:
- 所有非 in-out 參數(shù)處理完之后開始瘾敢,直到函數(shù)執(zhí)行完畢為止
- 有多個(gè) in-out 參數(shù),則寫訪問開始的順序與參數(shù)的順序一致
- 不能在訪問以 in-out 形式傳入后的原變量尿这,即使作用域原則和訪問權(quán)限允許
var stepSize = 1// 全局變量
func increment(_ number: inout Int) {
number += stepSize // stepSize 的讀訪問與 number 的寫訪問重疊了
}
increment(&stepSize)
// 錯(cuò)誤:stepSize 訪問沖突
-
number
和stepSize
都指向了同一個(gè)存儲(chǔ)地址 - 同一塊內(nèi)存的讀和寫訪問重疊了
image
- 解決 inout 參數(shù)訪問沖突:拷貝一份
stepSize
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
// stepSize is now 2
- 讀訪問在寫操作之前就已經(jīng)結(jié)束了簇抵,所以不會(huì)有沖突。
- 同一個(gè)函數(shù)的多個(gè) in-out 參數(shù)里傳入同一個(gè)變量射众,產(chǎn)生沖突
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) // 正常, 訪問的是不同的內(nèi)存位置
balance(&playerOneScore, &playerOneScore)// 同時(shí)訪問同一個(gè)的存儲(chǔ)地址碟摆。
// 錯(cuò)誤:playerOneScore 訪問沖突
操作符也是函數(shù),也會(huì)對(duì) in-out 參數(shù)進(jìn)行長(zhǎng)期訪問
balance(_:_:)
是一個(gè)名為<^>
的操作符函數(shù)叨橱,那么playerOneScore <^> playerOneScore
也會(huì)造成像balance(&playerOneScore, &playerOneScore)
一樣的沖突
方法里 self 的訪問沖突
- 本質(zhì):結(jié)構(gòu)體的 mutating 方法會(huì)在調(diào)用期間對(duì)
self
進(jìn)行寫訪問
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
-
不管有沒有調(diào)用 self典蜕,只要 標(biāo)記了mutating
- 在上面的
restoreHealth()
方法里,一個(gè)對(duì)于self
的寫訪問會(huì)從方法開始直到方法 return - 不可以對(duì)
Player
實(shí)例的屬性發(fā)起重疊的訪問
- 在上面的
shareHealth(with:)
接受另一個(gè)Player
的實(shí)例作為 in-out 參數(shù)罗洗,有訪問重疊的可能性
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
玩家的血量分享給maria
玩家- 方法調(diào)用時(shí)會(huì)對(duì)
oscar
發(fā)起寫訪問愉舔,在 mutating 方法里self
就是oscar
-
maria
也會(huì)發(fā)起寫訪問,因?yàn)?maria
作為 in-out 參數(shù)傳入 - 訪問內(nèi)存的不同位置伙菜。即使兩個(gè)寫訪問重疊了轩缤,它們也不會(huì)沖突
- 方法調(diào)用時(shí)會(huì)對(duì)
img
oscar.shareHealth(with: &oscar)
// 錯(cuò)誤:oscar 訪問沖突
-
self
和teammate
都指向了同一個(gè)存儲(chǔ)地址 - 同一塊內(nèi)存同時(shí)進(jìn)行兩個(gè)寫訪問,并且它們重疊了,就此產(chǎn)生了沖突
image
oscar.shareHealth(with: &oscar)
// 錯(cuò)誤:oscar 訪問沖突
-
self
和teammate
都指向了同一個(gè)存儲(chǔ)地址 - 同一塊內(nèi)存同時(shí)進(jìn)行兩個(gè)寫訪問火的,并且它們重疊了壶愤,就此產(chǎn)生了沖突
image
屬性的訪問沖突
- 出現(xiàn)場(chǎng)景:
- 值類型:結(jié)構(gòu)體,元組和枚舉馏鹤,由多個(gè)獨(dú)立的值組成
- 修改值的一部分都是對(duì)整個(gè)值的修改
- 一個(gè)屬性的讀或?qū)懺L問都需要訪問整一個(gè)值
- 如征椒,元組元素的寫訪問重疊會(huì)產(chǎn)生沖突:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 錯(cuò)誤:playerInformation 的屬性訪問沖突
- 傳入同一元組的元素對(duì)
balance(_:_:)
進(jìn)行調(diào)用,產(chǎn)生了沖突湃累,因?yàn)?playerInformation
的訪問產(chǎn)生了寫訪問重疊 - 作為 in-out 參數(shù)傳入
- 對(duì)于元組元素的寫訪問都需要對(duì)整個(gè)元組發(fā)起寫訪問
- 展示錯(cuò)誤:對(duì)于一個(gè)存儲(chǔ)在全局變量里的結(jié)構(gòu)體屬性的寫訪問重疊 (struct Player)
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 錯(cuò)誤
- 解決:將變量
holly
改為本地變量勃救,而非全局變量,
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // 正常
}
// 兩個(gè)存儲(chǔ)屬性任何情況下都不會(huì)相互影響(全局變量脱茉,傳指針剪芥,局部變量傳值)
- 遵循下面原則,編譯器可保證結(jié)構(gòu)體屬性的重疊訪問安全
- 訪問的是實(shí)例的存儲(chǔ)屬性琴许,而非計(jì)算屬性或類的屬性
- 結(jié)構(gòu)體是本地變量的值税肪,而非全局變量
- 結(jié)構(gòu)體要么沒有被閉包捕獲,要么只被非逃逸閉包捕獲了