案例代碼下載
內(nèi)存安全
默認(rèn)情況下飒筑,Swift可以防止代碼中發(fā)生不安全行為。例如,Swift確保變量在使用之前進(jìn)行初始化超营,在取消分配后不訪問(wèn)內(nèi)存,并檢查數(shù)組索引是否存在越界錯(cuò)誤阅虫。
Swift還確保對(duì)同一內(nèi)存區(qū)域的多次訪問(wèn)不會(huì)發(fā)生沖突演闭,因?yàn)樾枰薷膬?nèi)存中某個(gè)位置的代碼才能對(duì)該內(nèi)存進(jìn)行獨(dú)占訪問(wèn)。因?yàn)镾wift自動(dòng)管理內(nèi)存颓帝,所以大多數(shù)時(shí)候根本不需要考慮訪問(wèn)內(nèi)存米碰。但是,了解潛在沖突可能發(fā)生的位置非常重要购城,這樣就可以避免編寫(xiě)對(duì)內(nèi)存具有沖突訪問(wèn)權(quán)限的代碼吕座。如果代碼確實(shí)包含沖突,那么將收到編譯時(shí)或運(yùn)行時(shí)錯(cuò)誤瘪板。
了解對(duì)內(nèi)存的沖突訪問(wèn)
當(dāng)執(zhí)行諸如設(shè)置變量的值或?qū)?shù)傳遞給函數(shù)之類的操作時(shí)米诉,會(huì)在代碼中訪問(wèn)內(nèi)存。例如篷帅,以下代碼包含讀寫(xiě)內(nèi)存:
var one = 1
print("We're number \(one)!")
/*
打印結(jié)果:
We're number 1!
*/
當(dāng)代碼的不同部分試圖同時(shí)訪問(wèn)內(nèi)存中的相同位置時(shí)史侣,可能會(huì)發(fā)生沖突的內(nèi)存訪問(wèn)拴泌。同時(shí)多次訪問(wèn)內(nèi)存中的某個(gè)位置會(huì)產(chǎn)生不可預(yù)測(cè)或不一致的行為。在Swift中惊橱,有一些方法可以修改跨越多行代碼的值蚪腐,從而可以嘗試在自己修改的過(guò)程中訪問(wèn)一個(gè)值。
通過(guò)考慮如何更新寫(xiě)在紙上的預(yù)算税朴,可以看到類似的問(wèn)題回季。更新預(yù)算的過(guò)程分為兩步:首先添加項(xiàng)目的名稱和價(jià)格,然后更改總金額以反映列表中當(dāng)前的項(xiàng)目正林。在更新之前和之后泡一,可以從預(yù)算中讀取任何信息并獲得正確答案,如下圖所示觅廓。
當(dāng)在預(yù)算中添加項(xiàng)目時(shí)鼻忠,它處于臨時(shí)無(wú)效狀態(tài),因?yàn)榭偨痤~尚未更新以反映新添加的項(xiàng)目杈绸。在添加項(xiàng)目的過(guò)程中讀取總金額會(huì)導(dǎo)致不正確的信息帖蔓。
此示例還演示了在修復(fù)對(duì)內(nèi)存的沖突訪問(wèn)時(shí)可能遇到的挑戰(zhàn):有時(shí)有多種方法可以解決產(chǎn)生不同答案的沖突,并且并不總是很明顯哪個(gè)答案是正確的瞳脓。在此示例中塑娇,根據(jù)是否需要原始總金額或更新的總金額, 320可能是正確的答案劫侧。在修復(fù)沖突訪問(wèn)之前埋酬,必須確定要執(zhí)行的操作。
注意
如果編寫(xiě)了并發(fā)或多線程代碼烧栋,則對(duì)內(nèi)存的沖突訪問(wèn)可能是一個(gè)熟悉的問(wèn)題写妥。但是,此處討論的沖突訪問(wèn)可能發(fā)生在單個(gè)線程上劲弦,并且不涉及并發(fā)或多線程代碼。
如果在單個(gè)線程中存在對(duì)內(nèi)存的沖突訪問(wèn)醇坝,Swift會(huì)保證您在編譯時(shí)或運(yùn)行時(shí)都會(huì)收到錯(cuò)誤邑跪。對(duì)于多線程代碼,請(qǐng)使用Thread Sanitizer來(lái)幫助檢測(cè)跨線程的沖突訪問(wèn)呼猪。
內(nèi)存訪問(wèn)的特征
在沖突訪問(wèn)的上下文中要考慮存儲(chǔ)器訪問(wèn)的三個(gè)特征:訪問(wèn)是讀取還是寫(xiě)入画畅,訪問(wèn)的持續(xù)時(shí)間以及訪問(wèn)的存儲(chǔ)器中的位置。具體而言宋距,如果有兩個(gè)滿足以下所有條件的訪問(wèn)轴踱,則會(huì)發(fā)生沖突:
- 至少一個(gè)是寫(xiě)訪問(wèn)。
- 他們?cè)L問(wèn)內(nèi)存中的相同位置谚赎。
- 他們的持續(xù)時(shí)間重疊淫僻。
讀寫(xiě)訪問(wèn)之間的區(qū)別通常很明顯:寫(xiě)訪問(wèn)會(huì)更改內(nèi)存中的位置诱篷,但讀訪問(wèn)不會(huì)。內(nèi)存中的位置是指正在訪問(wèn)的內(nèi)容 - 例如雳灵,變量棕所,常量或?qū)傩浴4鎯?chǔ)器訪問(wèn)的持續(xù)時(shí)間是瞬時(shí)的或長(zhǎng)期的悯辙。
如果在訪問(wèn)開(kāi)始之后但在結(jié)束之前其他代碼無(wú)法運(yùn)行琳省,則訪問(wèn)是即時(shí)的。就其本質(zhì)而言躲撰,兩次即時(shí)訪問(wèn)不可能同時(shí)發(fā)生针贬。大多數(shù)內(nèi)存訪問(wèn)都是即時(shí)的。例如拢蛋,下面代碼清單中的所有讀寫(xiě)訪問(wèn)都是即時(shí)的:
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
/*
打印結(jié)果:
2
*/
但是桦他,有幾種方法可以訪問(wèn)內(nèi)存,稱為長(zhǎng)期訪問(wèn)瓤狐,跨越其他代碼的執(zhí)行瞬铸。即時(shí)訪問(wèn)和長(zhǎng)期訪問(wèn)之間的區(qū)別在于,其他代碼可以在長(zhǎng)期訪問(wèn)開(kāi)始之后但在結(jié)束之前運(yùn)行础锐,這稱為重疊嗓节。長(zhǎng)期訪問(wèn)可以與其他長(zhǎng)期訪問(wèn)和即時(shí)訪問(wèn)重疊。
重疊訪問(wèn)主要出現(xiàn)在使用函數(shù)和方法中的輸入輸出參數(shù)或結(jié)構(gòu)的mutating方法的代碼中皆警。使用長(zhǎng)期訪問(wèn)的特定Swift代碼類型將在下面的部分中討論拦宣。
對(duì)In-Out參數(shù)的訪問(wèn)沖突
函數(shù)具有對(duì)其所有輸入輸出參數(shù)的長(zhǎng)期寫(xiě)訪問(wèn)權(quán)。對(duì)in-out參數(shù)的寫(xiě)訪問(wèn)開(kāi)始于所有非in-out參數(shù)評(píng)估之后并且持續(xù)該函數(shù)調(diào)用的整個(gè)持續(xù)時(shí)間信姓。如果有多個(gè)輸入輸出參數(shù)鸵隧,則寫(xiě)訪問(wèn)的開(kāi)始順序與參數(shù)顯示的順序相同。
這種長(zhǎng)期寫(xiě)入訪問(wèn)的一個(gè)結(jié)果是無(wú)法訪問(wèn)作為輸入輸出傳遞的原始變量意推,即使范圍規(guī)則和訪問(wèn)控制允許它 - 任何對(duì)原始數(shù)據(jù)的訪問(wèn)都會(huì)產(chǎn)生沖突豆瘫。例如:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize) // 錯(cuò)誤:沖突反問(wèn)stepSize
在上面的代碼中,stepSize是一個(gè)全局變量菊值,它通惩馇可以從increment(_:)內(nèi)部訪問(wèn)。但是腻窒,讀訪問(wèn)stepSize與寫(xiě)訪問(wèn)重疊number昵宇。如在下文中的圖所示,兩者number并stepSize在存儲(chǔ)器中指代相同的位置儿子。讀和寫(xiě)訪問(wèn)指的是相同的內(nèi)存瓦哎,它們重疊,產(chǎn)生沖突。
解決這種沖突的一種方法是制作一份明確的副本stepSize:
var copyOfStepSize = stepSize // 制作一個(gè)副本
increment(©OfStepSize)
stepSize = copyOfStepSize
當(dāng)stepSize在increment(_:)調(diào)用之前復(fù)制蒋譬,很明顯割岛,copyOfStepSize的值通過(guò)當(dāng)前的stepSize增加。讀訪問(wèn)在寫(xiě)訪問(wèn)開(kāi)始之前結(jié)束羡铲,因此不存在沖突蜂桶。
對(duì)輸入輸出參數(shù)進(jìn)行長(zhǎng)期寫(xiě)訪問(wèn)的另一個(gè)后果是,將單個(gè)變量作為同一函數(shù)的多個(gè)輸入輸出參數(shù)的參數(shù)傳遞會(huì)產(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) // 可以
balance(&playerOneScore, &playerOneScore) // 錯(cuò)誤:沖突反問(wèn)playerOneScore
上面的balance(::)函數(shù)修改了它的兩個(gè)參數(shù)扑媚,以便在它們之間平均分配總值。使用playerOneScore和playerTwoScore作為參數(shù)調(diào)用它不會(huì)產(chǎn)生沖突 - 有兩個(gè)寫(xiě)訪問(wèn)在時(shí)間上重疊雷恃,但它們?cè)L問(wèn)內(nèi)存中的不同位置疆股。相反,playerOneScore作為兩個(gè)參數(shù)的值傳遞會(huì)產(chǎn)生沖突倒槐,因?yàn)樗噲D同時(shí)對(duì)內(nèi)存中的同一位置執(zhí)行兩次寫(xiě)訪問(wèn)旬痹。
注意
因?yàn)檫\(yùn)算符是函數(shù),所以它們也可以長(zhǎng)期訪問(wèn)其輸入輸出參數(shù)讨越。例如两残,如果balance(::)是一個(gè)名為<^>的運(yùn)算符函數(shù),則寫(xiě)playerOneScore <^> playerOneScore將導(dǎo)致與balance(&playerOneScore, &playerOneScore)相同的沖突把跨。
方法中的self沖突
結(jié)構(gòu)上的mutating方法在方法調(diào)用期間具有對(duì)self的寫(xiě)訪問(wèn)權(quán)人弓。例如,考慮一種游戲着逐,其中每個(gè)玩家具有健康量崔赌,其在受到傷害時(shí)減少,還具有能量值耸别,并且能量量在使用特殊能力時(shí)減少健芭。
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
在上面的restoreHealth()方法中,self寫(xiě)入訪問(wèn)從方法的開(kāi)頭開(kāi)始并持續(xù)到方法返回秀姐。在這種情況下慈迈,內(nèi)部沒(méi)有其他restoreHealth()代碼可以重疊訪問(wèn)Player實(shí)例的屬性。下面的shareHealth(with:)方法將另一個(gè)Player實(shí)例作為輸入輸出參數(shù)省有,從而創(chuàng)建重疊訪問(wè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) // 可以
在上面的示例中,調(diào)用Oscar游戲者的shareHealth(with:)方法與Maria共享健康狀態(tài)不會(huì)導(dǎo)致沖突锥咸。在方法調(diào)用期間對(duì)oscar存在寫(xiě)訪問(wèn)權(quán)狭瞎,因?yàn)閛scar是的mutating方法中self的值细移,并且maria在相同的持續(xù)時(shí)間內(nèi)存在寫(xiě)訪問(wèn)權(quán)搏予,因?yàn)樗黰aria是作為輸入輸出參數(shù)傳遞的。如下圖所示弧轧,它們?cè)L問(wèn)內(nèi)存中的不同位置雪侥。即使兩個(gè)寫(xiě)訪問(wèn)在時(shí)間上重疊碗殷,它們也不會(huì)發(fā)生沖突。
但是速缨,如果oscar作為參數(shù)傳遞給shareHealth(with:)锌妻,則存在沖突:
oscar.shareHealth(with: &oscar) // 錯(cuò)誤:沖突訪問(wèn)oscar
mutating方法需要在方法的持續(xù)時(shí)間內(nèi)對(duì)self進(jìn)行寫(xiě)訪問(wèn),并且需要對(duì)輸入輸出參數(shù)teammate相同持續(xù)時(shí)間的寫(xiě)訪問(wèn)權(quán)旬牲。在該方法中仿粹,無(wú)論是self和teammate指的是相同的位置在內(nèi)存如示于下圖中。兩次寫(xiě)訪問(wèn)指的是相同的內(nèi)存原茅,它們重疊吭历,產(chǎn)生沖突。
對(duì)屬性的沖突訪問(wèn)
結(jié)構(gòu)擂橘,元組和枚舉等類型由單個(gè)組成值組成晌区,例如結(jié)構(gòu)的屬性或元組的元素。因?yàn)檫@些是值類型通贞,所以改變值的任何部分都會(huì)改變整個(gè)值朗若,這意味著對(duì)其中一個(gè)屬性的讀取或?qū)懭朐L問(wèn)需要對(duì)整個(gè)值進(jìn)行讀取或?qū)懭朐L問(wèn)。例如昌罩,重疊對(duì)元組元素的寫(xiě)訪問(wèn)會(huì)產(chǎn)生沖突:
var playerInformation = (health: 10, enertgy: 10)
balance(&playerInformation.health, &playerInformation.enertgy) // 錯(cuò)誤:沖突訪問(wèn)playerInformation
在上面的示例中哭懈,調(diào)用balance(::)元組的元素會(huì)產(chǎn)生沖突,因?yàn)榇嬖谥丿B的playerInformation寫(xiě)訪問(wèn)峡迷。playerInformation.health和playerInformation.energy雙方都在出參數(shù)银伟,這意味著調(diào)用balance(::)函數(shù)的持續(xù)時(shí)間需要寫(xiě)訪問(wèn)。在這兩種情況下绘搞,對(duì)元組元素的寫(xiě)訪問(wèn)都需要對(duì)整個(gè)元組進(jìn)行寫(xiě)訪問(wèn)彤避。這意味著有兩次寫(xiě)訪問(wèn)playerInformation,持續(xù)時(shí)間重疊夯辖,導(dǎo)致沖突琉预。
下面顯示的代碼,對(duì)于存儲(chǔ)在全局變量中的結(jié)構(gòu)屬性的重寫(xiě)寫(xiě)訪問(wèn)蒿褂,會(huì)出現(xiàn)相同的錯(cuò)誤圆米。
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 錯(cuò)誤
實(shí)際上,大多數(shù)對(duì)結(jié)構(gòu)屬性的訪問(wèn)都可以安全地重疊啄栓。例如娄帖,如果上例中的變量holly更改為局部變量而不是全局變量,則編譯器可以證明對(duì)結(jié)構(gòu)的存儲(chǔ)屬性的重疊訪問(wèn)是安全的:
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // 可以
}
someFunction()
在上面的例子中昙楚,oscar的健康和能量作為兩個(gè)in-out參數(shù)傳遞給balance(::)近速。編譯器可以證明保留了內(nèi)存安全性,因?yàn)閮蓚€(gè)存儲(chǔ)的屬性不會(huì)以任何方式進(jìn)行交互。
為了保持存儲(chǔ)器安全性削葱,并不總是必須限制對(duì)結(jié)構(gòu)屬性的重疊訪問(wèn)奖亚。內(nèi)存安全是理想的保證,但獨(dú)占訪問(wèn)是比內(nèi)存安全更嚴(yán)格的要求 - 這意味著一些代碼可以保持內(nèi)存安全析砸,即使它違反了內(nèi)存的獨(dú)占訪問(wèn)權(quán)限昔字。如果編譯器能夠證明對(duì)內(nèi)存的非獨(dú)占訪問(wèn)仍然是安全的,那么Swift允許這種內(nèi)存安全的代碼首繁。具體而言作郭,如果滿足以下條件,則可以證明對(duì)結(jié)構(gòu)屬性的重疊訪問(wèn)是安全的:
- 只訪問(wèn)實(shí)例的存儲(chǔ)屬性弦疮,而不是計(jì)算屬性或類屬性所坯。
- 結(jié)構(gòu)是局部變量的值,而不是全局變量挂捅。
- 結(jié)構(gòu)要么不被任何閉包捕獲芹助,要么僅由非逃逸閉包捕獲。
如果編譯器無(wú)法證明訪問(wèn)是安全的闲先,則不允許訪問(wèn)状土。