本文主要翻譯今年 The Swift Programming Language (Swift 4) 中新出的章節(jié) -《Memory Safety》。在 Swift 4 中,內(nèi)存安全訪問進(jìn)行很大的優(yōu)化《What's New in Swift 4 ?》。
默認(rèn)情況下,Swift 會克服代碼層面上的一些不安全的行為,如:確保一個(gè)變量被初始化完后才能被訪問、確保變量在銷毀后不會被訪問等等安全操作。
Swift 也會確保在?多路訪問內(nèi)存中同一區(qū)域時(shí)不會沖突(獨(dú)占訪問??該區(qū)域)。通常情況下,我們完全無需考慮內(nèi)存訪問?沖突的問題,因?yàn)?Swift 是自動(dòng)管理內(nèi)存的。然而,在?碼代碼的時(shí)候,了解那些地方可能發(fā)生內(nèi)存訪問沖突是非常重要的。通常情況下,如果你的代碼有?內(nèi)存訪問沖突,那么 Xcode 會提示編譯?錯(cuò)誤或者運(yùn)行時(shí)錯(cuò)誤。
本文不會介紹什么是內(nèi)存?訪問沖突。詳見 The Swift Programming Language (Swift 4)。如果你寫的是并發(fā)或者多線程的程序,內(nèi)存沖突訪問與單線程是非常相似的一個(gè)問題。本文主要討論?單線程上的內(nèi)存沖突訪問。如果想檢測多線程是否存在內(nèi)存訪問沖突,你可以看看這篇文檔。
我們可以把訪問分為兩種:即時(shí)和長期(instantaneous & long-term)
- 即時(shí)訪問:即在訪問開始至?結(jié)束前都不可能有其他?代碼來?訪問同一區(qū)域。
- 長期訪問:即在訪問開始至?結(jié)束前可能有其他代碼來訪問同一區(qū)域。??長期訪問可能和其他即時(shí)訪問或者長期?訪問重疊。
重疊訪問主要帶有 in-out 參數(shù)的函數(shù)(或方法)以及結(jié)構(gòu)體中帶有 mutating 關(guān)鍵字的方法。我們下面來看看?例子。
In-Out 參數(shù)的訪問沖突
?一個(gè)函數(shù)對其 in-out 參數(shù)具有長期的訪問權(quán)限,如下代碼:
Excerpt From: Apple Inc. "The Swift Programming Language (Swift 4).” iBooks".
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
在上述代碼中,stepSize 是一個(gè)全局變量,而且被作為一個(gè) in-out 參數(shù)傳給 increment(_:) 方法。沖突的原因在于 number 和 stepSize 引用的是?內(nèi)存中同一區(qū)域,并且同時(shí)進(jìn)行讀寫訪問,因此導(dǎo)致訪問沖突。

我們可以?采用復(fù)制 stepSize 的方式解決該問題:
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
// stepSize is now 2
?self 的訪問沖突
在結(jié)構(gòu)體中,帶有 mutating 關(guān)鍵字的方法調(diào)用期間對 self 具有寫入權(quán)限。
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) // OK
?上述代碼是 Ok 的,?即時(shí)寫入權(quán)限在時(shí)間上是重疊的,但是是分別訪問 oscar 的 health 和 maria 的 health,因此在 shareHealth(with:) 方法中并沒有發(fā)生內(nèi)存訪問沖突。

然而,如果你把 oscar 作為參數(shù)傳給 shareHealth(with:),那么?就會產(chǎn)生內(nèi)存訪問沖突:
oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
很顯然,shareHealth(with:) 方法中的 ?self 和 teammate 同時(shí)指向內(nèi)存中同一區(qū)域,即同時(shí)對 oscar 的 health 進(jìn)行讀寫訪問,因此導(dǎo)致訪問沖突。

屬性的訪問沖突
像結(jié)構(gòu)體、?元組、枚舉這些類型都是由各個(gè)值組成的,如:結(jié)構(gòu)體的?各種屬性、元組的各種元素等等。因?yàn)?它們都是值類型,這意味著對其中一個(gè)屬性的讀寫訪問就是對整個(gè)值進(jìn)行讀寫訪問。代碼如下:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
上述代碼不難理解,因?yàn)樵媸侵殿愋停鲜?balance(_:_:) 發(fā)生內(nèi)存訪問沖突,即同時(shí)訪問 playerInformation。
下面我們再看一下結(jié)構(gòu)體,其中 holly 是一個(gè)全局變量
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // Error
上述代碼會報(bào)這樣一個(gè)錯(cuò)誤:Simultaneous accesses to 0x10****580, but modification requires exclusive access。?其實(shí)就是內(nèi)存訪問沖突了,Swift 4 中也針對這塊做了優(yōu)化處理,?感興趣的同學(xué)可以?查閱我之前寫的一篇文章《[WWDC17] What's New in Swift 4 ?》。
在實(shí)踐中,上述代碼中的 holly 一般是個(gè)局部變量而非全局變量,編譯器可以保證對結(jié)構(gòu)體的存儲屬性?進(jìn)行重疊訪問是安全的,代碼如下:
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK
}
上述代碼?運(yùn)行是 Ok 的,有時(shí)候,限制?結(jié)構(gòu)體的各屬性進(jìn)行重疊訪問是沒有必要的,這也就是為什么 someFunction() 沒有發(fā)生沖突訪問的原因。內(nèi)存訪問安全雖應(yīng)當(dāng)?shù)玫奖WC,但是獨(dú)占訪問比內(nèi)存?安全訪問要求更加嚴(yán)格,從上述代碼可看出,即時(shí)違背了獨(dú)占訪問的原則,內(nèi)存安全也能得到保證。一般情況下,編譯器會在如下條件下保證對結(jié)構(gòu)體的存儲屬性?進(jìn)行安全的重疊訪問:
- 只訪問某個(gè)實(shí)例的存儲屬性,而不是計(jì)算屬性或類屬性
- 訪問的是?局部的結(jié)構(gòu)體變量,而不是全局變量
- 結(jié)構(gòu)體沒有被任何閉包所捕獲,或者僅被非逃逸閉包捕獲。
感興趣的同學(xué)可以查閱這里 The Swift Programming Language (Swift 4)。