# 內存安全
默認情況下,Swift 會阻止你代碼里不安全的行為。例如,Swift 會保證變量在使用之前就完成初始化,在內存被回收之后就無法被訪問,并且數組的索引會做越界檢查。
Swift 也保證同時訪問同一塊內存時不會沖突,通過約束代碼里對于存儲地址的寫操作,去獲取那一塊內存的訪問獨占權。因為 Swift 自動管理內存,所以大部分時候你完全不需要考慮內存訪問的事情。然而,理解潛在的沖突也是很重要的,可以避免你寫出訪問沖突的代碼。而如果你的代碼確實存在沖突,那在編譯時或者運行時就會得到錯誤。
## 理解內存訪問沖突 {#understanding-conflicting-access-to-memory}
內存的訪問,會發生在你給變量賦值,或者傳遞參數給函數時。例如,下面的代碼就包含了讀和寫的訪問:
```swift
// 向 one 所在的內存區域發起一次寫操作
var one = 1
// 向 one 所在的內存區域發起一次讀操作
print("We're number \(one)!")
```
內存訪問的沖突會發生在你的代碼嘗試同時訪問同一個存儲地址的時侯。同一個存儲地址的多個訪問同時發生會造成不可預計或不一致的行為。在 Swift 里,有很多修改值的行為都會持續好幾行代碼,在修改值的過程中進行訪問是有可能發生的。
你可以思考一下預算表更新的過程,會看到同樣的問題。更新預算表總共有兩步:首先你把預算項的名字和費用加上,然后再更新總數來反映預算表的現況。在更新之前和之后,你都可以從預算表里讀取任何信息并獲得正確的答案,就像下面展示的那樣。

而當你添加預算項進入表里的時候,它只是在一個臨時的,錯誤的狀態,因為總數還沒有被更新。在添加數據的過程中讀取總數就會讀取到錯誤的信息。
這個例子也演示了你在修復內存訪問沖突時會遇到的問題:有時修復的方式會有很多種,但哪一種是正確的就不總是那么明顯了。在這個例子里,根據你是否需要更新后的總數,$5 和 $320 都可能是正確的值。在你修復訪問沖突之前,你需要決定它的傾向。
> 注意
>
> 如果你寫過并發和多線程的代碼,內存訪問沖突也許是同樣的問題。然而,這里訪問沖突的討論是在單線程的情境下討論的,并沒有使用并發或者多線程。
>
> 如果你曾經在單線程代碼里有訪問沖突,Swift 可以保證你在編譯或者運行時會得到錯誤。對于多線程的代碼,可以使用 [Thread Sanitizer](https://developer.apple.com/documentation/code_diagnostics/thread_sanitizer) 去幫助檢測多線程的沖突。
### 內存訪問性質 {#characteristics-of-memory-access}
內存訪問沖突時,要考慮內存訪問上下文中的這三個性質:訪問是讀還是寫,訪問的時長,以及被訪問的存儲地址。特別是,沖突會發生在當你有兩個訪問符合下列的情況:
* 至少有一個是寫訪問
* 它們訪問的是同一個存儲地址
* 它們的訪問在時間線上部分重疊
讀和寫訪問的區別很明顯:一個寫訪問會改變存儲地址,而讀操作不會。存儲地址是指向正在訪問的東西(例如一個變量,常量或者屬性)的位置的值 。內存訪問的時長要么是瞬時的,要么是長期的。
如果一個訪問不可能在其訪問期間被其它代碼訪問,那么就是一個瞬時訪問。正常來說,兩個瞬時訪問是不可能同時發生的。大多數內存訪問都是瞬時的。例如,下面列舉的所有讀和寫訪問都是瞬時的:
```swift
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// 打印“2”
```
然而,有幾種被稱為長期訪問的內存訪問方式,會在別的代碼執行時持續進行。瞬時訪問和長期訪問的區別在于別的代碼有沒有可能在訪問期間同時訪問,也就是在時間線上的重疊。一個長期訪問可以被別的長期訪問或瞬時訪問重疊。
重疊的訪問主要出現在使用 in-out 參數的函數和方法或者結構體的 mutating 方法里。Swift 代碼里典型的長期訪問會在后面進行討論。
## In-Out 參數的訪問沖突 {#conflicting-access-to-in-out-parameters}
一個函數會對它所有的 in-out 參數進行長期寫訪問。in-out 參數的寫訪問會在所有非 in-out 參數處理完之后開始,直到函數執行完畢為止。如果有多個 in-out 參數,則寫訪問開始的順序與參數的順序一致。
長期訪問的存在會造成一個結果,你不能在訪問以 in-out 形式傳入后的原變量,即使作用域原則和訪問權限允許——任何訪問原變量的行為都會造成沖突。例如:
```swift
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// 錯誤:stepSize 訪問沖突
```
在上面的代碼里,`stepSize` 是一個全局變量,并且它可以在 `increment(_:)` 里正常訪問。然而,對于 `stepSize` 的讀訪問與 `number` 的寫訪問重疊了。就像下面展示的那樣,`number` 和 `stepSize` 都指向了同一個存儲地址。同一塊內存的讀和寫訪問重疊了,就此產生了沖突。

解決這個沖突的一種方式,是顯示拷貝一份 `stepSize` :
```swift
// 顯式拷貝
var copyOfStepSize = stepSize
increment(©OfStepSize)
// 更新原來的值
stepSize = copyOfStepSize
// stepSize 現在的值是 2
```
當你在調用 `increment(_:)` 之前做一份拷貝,顯然 `copyOfStepSize` 就會根據當前的 `stepSize` 增加。讀訪問在寫操作之前就已經結束了,所以不會有沖突。
長期寫訪問的存在還會造成另一種結果,往同一個函數的多個 in-out 參數里傳入同一個變量也會產生沖突,例如:
```swift
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)
// 錯誤:playerOneScore 訪問沖突
```
上面的 `balance(_:_:)` 函數會將傳入的兩個參數平均化。將 `playerOneScore` 和 `playerTwoScore` 作為參數傳入不會產生錯誤 —— 有兩個訪問重疊了,但它們訪問的是不同的內存位置。相反,將 `playerOneScore` 作為參數同時傳入就會產生沖突,因為它會發起兩個寫訪問,同時訪問同一個的存儲地址。
> 注意
>
> 因為操作符也是函數,它們也會對 in-out 參數進行長期訪問。例如,假設 `balance(_:_:)` 是一個名為 `<^>` 的操作符函數,那么 `playerOneScore <^> playerOneScore` 也會造成像 `balance(&playerOneScore, &playerOneScore)` 一樣的沖突。
## 方法里 self 的訪問沖突 {#conflicting-access-to-self-in-methods}
一個結構體的 mutating 方法會在調用期間對 `self` 進行寫訪問。例如,想象一下這么一個游戲,每一個玩家都有血量,受攻擊時血量會下降,并且有敵人的數量,使用特殊技能時會減少敵人數量。
```swift
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
```
在上面的 `restoreHealth()` 方法里,一個對于 `self` 的寫訪問會從方法開始直到方法 return。在這種情況下,`restoreHealth()` 里的其它代碼不可以對 `Player` 實例的屬性發起重疊的訪問。下面的 `shareHealth(with:)` 方法接受另一個 `Player` 的實例作為 in-out 參數,產生了訪問重疊的可能性。
```swift
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) // 正常
```
上面的例子里,調用 `shareHealth(with:)` 方法去把 `oscar` 玩家的血量分享給 `maria` 玩家并不會造成沖突。在方法調用期間會對 `oscar` 發起寫訪問,因為在 mutating 方法里 `self` 就是 `oscar`,同時對于 `maria` 也會發起寫訪問,因為 `maria` 作為 in-out 參數傳入。過程如下,它們會訪問內存的不同位置。即使兩個寫訪問重疊了,它們也不會沖突。

當然,如果你將 `oscar` 作為參數傳入 `shareHealth(with:)` 里,就會產生沖突:
```swift
oscar.shareHealth(with: &oscar)
// 錯誤:oscar 訪問沖突
```
mutating 方法在調用期間需要對 `self` 發起寫訪問,而同時 in-out 參數也需要寫訪問。在方法里,`self` 和 `teammate` 都指向了同一個存儲地址——就像下面展示的那樣。對于同一塊內存同時進行兩個寫訪問,并且它們重疊了,就此產生了沖突。

## 屬性的訪問沖突 {#conflicting-access-to-properties}
如結構體,元組和枚舉的類型都是由多個獨立的值組成的,例如結構體的屬性或元組的元素。因為它們都是值類型,修改值的任何一部分都是對于整個值的修改,意味著其中一個屬性的讀或寫訪問都需要訪問整一個值。例如,元組元素的寫訪問重疊會產生沖突:
```swift
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// 錯誤:playerInformation 的屬性訪問沖突
```
上面的例子里,傳入同一元組的元素對 `balance(_:_:)` 進行調用,產生了沖突,因為 `playerInformation` 的訪問產生了寫訪問重疊。`playerInformation.health` 和 `playerInformation.energy` 都被作為 in-out 參數傳入,意味著 `balance(_:_:)` 需要在函數調用期間對它們發起寫訪問。任何情況下,對于元組元素的寫訪問都需要對整個元組發起寫訪問。這意味著對于 `playerInfomation` 發起的兩個寫訪問重疊了,造成沖突。
下面的代碼展示了一樣的錯誤,對于一個存儲在全局變量里的結構體屬性的寫訪問重疊了。
```swift
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // 錯誤
```
在實踐中,大多數對于結構體屬性的訪問都會安全的重疊。例如,將上面例子里的變量 `holly` 改為本地變量而非全局變量,編譯器就會可以保證這個重疊訪問是安全的:
```swift
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // 正常
}
```
上面的例子里,`oscar` 的 `health` 和 `energy` 都作為 in-out 參數傳入了 `balance(_:_:)` 里。編譯器可以保證內存安全,因為兩個存儲屬性任何情況下都不會相互影響。
限制結構體屬性的重疊訪問對于保證內存安全不是必要的。保證內存安全是必要的,但因為訪問獨占權的要求比內存安全還要更嚴格——意味著即使有些代碼違反了訪問獨占權的原則,也是內存安全的,所以如果編譯器可以保證這種非專屬的訪問是安全的,那 Swift 就會允許這種行為的代碼運行。特別是當你遵循下面的原則時,它可以保證結構體屬性的重疊訪問是安全的:
* 你訪問的是實例的存儲屬性,而不是計算屬性或類的屬性
* 結構體是本地變量的值,而非全局變量
* 結構體要么沒有被閉包捕獲,要么只被非逃逸閉包捕獲了
如果編譯器無法保證訪問的安全性,它就不會允許那次訪問。
- 1.關于 Swift
- 2.Swift 初見
- 2-1基礎部分
- 2-2基本運算符
- 2-3字符串和字符
- 2-4集合類型
- 2-5控制流
- 2-6函數
- 2-7閉包
- 2-8枚舉
- 2-9類和結構體
- 2-10屬性
- 2-11方法
- 2-12下標
- 2-13繼承
- 2-14構造過程
- 2-15析構過程
- 2-16可選鏈
- 2-17錯誤處理
- 2-18類型轉換
- 2-19嵌套類型
- 2-20擴展
- 2-21協議
- 2-22泛型
- 2-23不透明類型
- 2-24自動引用計數
- 2-25內存安全
- 2-26訪問控制
- 2-27高級運算符
- 3-1關于語言參考
- 3-2詞法結構
- 3-3類型
- 3-4表達式
- 3-5語句
- 3-6聲明
- 3-7特性
- 3-8模式
- 3-9泛型參數
- 4語法總結