默認(rèn)情況下承边,Swift可以防止代碼中出現(xiàn)不安全行為。例如毡鉴,Swift確保變量在使用之前被初始化崔泵,內(nèi)存在被釋放后不被訪問秒赤,數(shù)組索引被檢查是否越界。
Swift還確保對(duì)同一內(nèi)存區(qū)域的多次訪問不會(huì)發(fā)生沖突憎瘸,這是因?yàn)橹挥行枰薷膬?nèi)存中某個(gè)位置的代碼才擁有對(duì)該內(nèi)存的訪問權(quán)限入篮。因?yàn)镾wift是自動(dòng)管理內(nèi)存的,通常情況下根本不需要考慮訪問內(nèi)存幌甘。但是潮售,了解潛在沖突可能發(fā)生在哪里是很重要的,這樣可以避免編寫訪問內(nèi)存的沖突代碼含潘。如果代碼中確實(shí)包含沖突饲做,則在編譯或運(yùn)行時(shí)發(fā)生錯(cuò)誤。
一遏弱、內(nèi)存訪問沖突
當(dāng)執(zhí)行諸如設(shè)置變量值或向函數(shù)傳遞參數(shù)等操作時(shí),就會(huì)在代碼中發(fā)生對(duì)內(nèi)存的訪問塞弊。例如漱逸,以下代碼包含讀訪問和寫訪問:
// A write access to the memory where one is stored.
var one = 1
// A read access from the memory where one is stored.
print("We're number \(one)!")
當(dāng)代碼的不同部分試圖同時(shí)訪問內(nèi)存中的相同位置時(shí),可能會(huì)發(fā)生內(nèi)存訪問沖突游沿。同時(shí)多次訪問內(nèi)存中的某個(gè)位置會(huì)產(chǎn)生不可預(yù)測(cè)或不一致的行為饰抒。在Swift中,有一些方法可以修改一個(gè)跨越多行代碼的值诀黍,從而可以嘗試在自己的修改過程中訪問一個(gè)值袋坑。
這個(gè)例子還演示了在修復(fù)內(nèi)存的訪問沖突時(shí)可能遇到的一個(gè)挑戰(zhàn):有時(shí)有多種方法可以解決沖突骑篙,同時(shí)也產(chǎn)生不同的答案蜕提,而且無法確定哪個(gè)答案是正確的。在本例中替蛉,根據(jù)您想要原始的總額還是更新后的總額贯溅,5美元或320美元可能是正確的答案拄氯。在修復(fù)沖突訪問之前,你必須確定要執(zhí)行的操作它浅。
注意:
如果你編寫過并發(fā)或多線程代碼译柏,則對(duì)內(nèi)存的沖突訪問可能是一個(gè)熟悉的問題。但是姐霍,此處討論的沖突訪問可能發(fā)生在單個(gè)線程上鄙麦,并且不涉及并發(fā)或多線程代碼。
如果在單線程中存在內(nèi)存訪問沖突镊折,Swift會(huì)保證在編譯時(shí)或運(yùn)行時(shí)都會(huì)收到錯(cuò)誤胯府。對(duì)于多線程代碼,請(qǐng)使用Thread Sanitizer幫助檢測(cè)跨線程的沖突訪問恨胚。
二骂因、內(nèi)存訪問的特征
在沖突訪問的上下文中,需要考慮內(nèi)存訪問的三個(gè)特征:訪問是讀還是寫赃泡,訪問的持續(xù)時(shí)間寒波,以及在內(nèi)存中的訪問位置。具體來說升熊,如果您有兩個(gè)滿足以下所有條件的訪問俄烁,就會(huì)發(fā)生沖突:
- 至少有一個(gè)是寫訪問。
- 它們?cè)L問內(nèi)存中的相同位置级野。
- 它們的時(shí)間重疊页屠。
讀訪問和寫訪問之間的區(qū)別通常很明顯:寫訪問改變內(nèi)存中的位置,但讀訪問不會(huì)蓖柔。內(nèi)存中的位置是指正在訪問的內(nèi)容 - 例如辰企,變量,常量或?qū)傩栽ǔ椤?nèi)存訪問的持續(xù)時(shí)間可以是瞬時(shí)的蟆豫,也可以是長(zhǎng)期的。
如果在訪問開始之后但在訪問結(jié)束之前不能運(yùn)行其他代碼懒闷,則訪問是瞬時(shí)的十减。從本質(zhì)上講,兩次瞬時(shí)訪問不可能同時(shí)發(fā)生愤估。大多數(shù)內(nèi)存訪問是瞬時(shí)的帮辟。例如,下面代碼清單中的所有讀寫訪問都是瞬時(shí)的:
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
但是玩焰,有幾種訪問內(nèi)存的方法由驹,稱為長(zhǎng)期訪問,可以跨越其他代碼的執(zhí)行。瞬時(shí)訪問和長(zhǎng)期訪問的區(qū)別在于蔓榄,其他代碼可以在長(zhǎng)期訪問開始后結(jié)束之前運(yùn)行并炮,這稱為重疊。長(zhǎng)期訪問可以與其他長(zhǎng)期訪問和瞬時(shí)訪問重疊甥郑。
重疊訪問主要出現(xiàn)在在函數(shù)和方法中使用in-out參數(shù)的代碼中逃魄,或者是結(jié)構(gòu)體的可變方法中。使用長(zhǎng)期訪問的特定Swift代碼類型將在下面的部分中討論澜搅。
三伍俘、對(duì)In-Out參數(shù)的訪問沖突
函數(shù)擁有對(duì)其所有in-out參數(shù)的長(zhǎng)期寫訪問權(quán)。一個(gè)in-out參數(shù)的寫訪問在所有非in-out參數(shù)被計(jì)算之后開始勉躺,并持續(xù)到整個(gè)函數(shù)調(diào)用期間癌瘾。如果有多個(gè)in-out參數(shù),則寫訪問的開始順序與參數(shù)出現(xiàn)的順序相同饵溅。
這種長(zhǎng)期寫訪問的一個(gè)后果是妨退,您不能訪問作為in-out傳遞的原始變量,即使范圍規(guī)則和訪問控制允許這樣做——任何對(duì)原始變量的訪問都會(huì)產(chǎn)生沖突概说。例如:
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
在上面的代碼中碧注,stepSize
是一個(gè)全局變量,通程桥猓可以從increment(_:)
訪問它。但是轩端,對(duì)stepSize
的讀訪問與對(duì)number
的寫訪問重疊放典。如下圖所示,number
和stepSize
都指向內(nèi)存中的同一個(gè)位置基茵。讀和寫訪問引用相同的內(nèi)存奋构,它們重疊,產(chǎn)生沖突拱层。
解決這個(gè)沖突的一種方法是顯式復(fù)制
stepSize
:
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
當(dāng)在調(diào)用increment(_:)
之前復(fù)制一個(gè)stepSize
時(shí)弥臼,很明顯copyOfStepSize
的值是由當(dāng)前stepSize
遞增的。讀訪問在寫訪問開始之前結(jié)束根灯,所以沒有沖突径缅。
對(duì)in-out參數(shù)進(jìn)行長(zhǎng)期寫訪問的另一個(gè)后果是,將單個(gè)變量作為同一個(gè)函數(shù)的多個(gè)in-out參數(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) // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore
上面balance(_:_:)
函數(shù)修改了它的兩個(gè)參數(shù)纳猪,使它們之間的值相等。用playerOneScore
和playerTwoScore
調(diào)用它不會(huì)產(chǎn)生沖突——有兩個(gè)寫訪問在時(shí)間上重疊桃笙,但它們?cè)L問的是不同的內(nèi)存位置氏堤。相反,將playerOneScore
作為兩個(gè)參數(shù)的值傳遞會(huì)產(chǎn)生沖突搏明,因?yàn)樗噲D同時(shí)執(zhí)行對(duì)內(nèi)存中相同位置的兩次寫訪問鼠锈。
注意:
由于操作符是函數(shù)闪檬,它們也可以長(zhǎng)期訪問它們的in-out參數(shù)。
因?yàn)檫\(yùn)算符是函數(shù)购笆,所以它們也可以長(zhǎng)期訪問其in-out參數(shù)粗悯。例如,如果balance(_:_:)
是一個(gè)名為<^>
的運(yùn)算符函數(shù)由桌,則寫入playerOneScore <^> playerOneScore
將導(dǎo)致與balance(&playerOneScore, &playerOneScore)
相同的沖突为黎。
四、方法中self
的訪問沖突
結(jié)構(gòu)體上的可變方法在方法調(diào)用期間對(duì)self
具有寫訪問權(quán)行您。例如铭乾,考慮這樣一個(gè)游戲,每個(gè)玩家都有一個(gè)生命值(在受到傷害時(shí)減少)和一個(gè)能量值(在使用特殊技能時(shí)減少)娃循。
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
在上面的restoreHealth()
方法中炕檩,對(duì)self
的寫訪問從方法的開頭開始,一直持續(xù)到方法返回為止捌斧。在本例中笛质,restoreHealth()
中沒有其他代碼可以重疊訪問Player
實(shí)例的屬性。下面的 shareHealth(with:)
方法將另一個(gè)Player
實(shí)例作為一個(gè)in-out參數(shù)捞蚂,創(chuàng)建了重疊訪問的可能性妇押。
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
在上面的例子中,調(diào)用Oscar玩家的shareHealth(with:)
方法與Maria 玩家共享生命值不會(huì)引起沖突姓迅。在方法調(diào)用期間有對(duì)oscar
的寫訪問敲霍,因?yàn)?code>oscar是可變方法中的self
值,在相同的時(shí)間內(nèi)也有對(duì)maria
的寫訪問丁存,因?yàn)?code>maria是作為 in-out
參數(shù)傳遞的肩杈。如下圖所示,它們?cè)L問內(nèi)存中的不同位置解寝。盡管兩個(gè)寫訪問在時(shí)間上是重疊的男翰,但它們并不沖突绑谣。
但是,如果將oscar
作為參數(shù)傳遞給shareHealth(with:)
,則存在沖突:
oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
在該方法的持續(xù)時(shí)間內(nèi)纤泵,可變方法需要對(duì)self
的寫訪問薯定。而在相同的持續(xù)時(shí)間內(nèi)毡泻,in-out參數(shù)需要對(duì)teammate
的寫訪問刘离。在方法中,self
和teamate
引用內(nèi)存中的相同位置—如下圖所示抑片。這兩個(gè)寫訪問引用相同的內(nèi)存卵佛,它們重疊,產(chǎn)生沖突。
五截汪、屬性的訪問沖突
結(jié)構(gòu)體疾牲、元組和枚舉等類型由單個(gè)組成值組成,例如結(jié)構(gòu)體的屬性或元組的元素衙解。由于這些都是值類型阳柔,因此改變值的任何部分會(huì)改變整個(gè)值,這意味著對(duì)其中一個(gè)屬性的讀或?qū)懺L問需要對(duì)整個(gè)值進(jìn)行讀或?qū)懺L問蚓峦。例如舌剂,對(duì)元組元素的重疊寫訪問會(huì)產(chǎn)生沖突:
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
在上面的例子中,調(diào)用元組元素上的balance(_:_:)
會(huì)產(chǎn)生沖突暑椰,因?yàn)閷?duì)playerInformation
的寫訪問有重疊霍转。playerInformation.health
和playerInformation.energy
作為in-out參數(shù)傳遞,這意味著balance(_:_:)
在函數(shù)調(diào)用期間需要對(duì)它們進(jìn)行寫訪問一汽。在這兩種情況下避消,對(duì)tuple元素的寫訪問都需要對(duì)整個(gè)tuple進(jìn)行寫訪問。這意味著有兩個(gè)對(duì)playerInformation
的寫訪問召夹,它們的持續(xù)時(shí)間重疊岩喷,導(dǎo)致沖突。
下面的代碼顯示监憎,對(duì)存儲(chǔ)在全局變量中的結(jié)構(gòu)體屬性的重疊寫訪問纱意,會(huì)出現(xiàn)相同的錯(cuò)誤。
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // Error
實(shí)際上鲸阔,大多數(shù)對(duì)結(jié)構(gòu)體屬性的訪問都可以安全地重疊妇穴。例如,如果上例中的變量holly
更改為局部變量而不是全局變量隶债,則編譯器可以證明對(duì)結(jié)構(gòu)體存儲(chǔ)屬性的重疊訪問是安全的:
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK
}
在上面的例子中,oscar
的 health
和energy
作為兩個(gè)輸入?yún)?shù)傳遞給balance(_:_:)
跑筝。編譯器可以證明保留了內(nèi)存安全性死讹,因?yàn)檫@兩個(gè)存儲(chǔ)屬性不以任何方式交互。
反對(duì)重疊訪問結(jié)構(gòu)體屬性的限制并不總是保證內(nèi)存安全的必要條件曲梗。內(nèi)存安全是理想的保證赞警,但唯一的訪問是比內(nèi)存安全更嚴(yán)格的要求 - 這意味著一些代碼可以維護(hù)內(nèi)存安全,即使它違反了對(duì)內(nèi)存的唯一訪問權(quán)限虏两。如果編譯器可以證明對(duì)內(nèi)存的非單獨(dú)訪問仍然是安全的愧旦,那么Swift允許這種內(nèi)存安全的代碼。具體而言定罢,如果滿足以下條件笤虫,則可以證明對(duì)結(jié)構(gòu)屬性的重疊訪問是安全的:
- 只訪問實(shí)例的存儲(chǔ)屬性,而不是計(jì)算屬性或類屬性。
- 結(jié)構(gòu)體是局部變量的值琼蚯,而不是全局變量酬凳。
- 該結(jié)構(gòu)體要么不被任何閉包捕獲,要么僅由非逃離閉包捕獲遭庶。
如果編譯器無法證明訪問是安全的宁仔,則不允許訪問。