Swift5.2 拾遺筆記(四)

本文為私人學習筆記,僅僅做為記錄使用,詳情內容請查閱 中文官方文檔


內存安全

Swift 自動管理內存,大部分時候你完全不需要考慮內存訪問的事情。并且在默認情況下,Swift 是安全的,它會阻止你代碼不安全的代碼,例如,Swift 需要你保證變量在使用前完成初始化,數據索引會做越界檢測等。

這里討論的是單線程下的內存安全。一般情況下,單線程是不會出現內存訪問下的安全問題。但是在一些特定條件下,依然會發生內存訪問沖突問題。沖突問題會發生在當你有兩個符合下列情況:

  • 至少有一個是寫訪問
  • 它們訪問的是同一個存儲地址
  • 它們的訪問在時間上部分重疊

重疊訪問

導致重疊訪問的主要問題是在使用 in-out 參數的函數或者方法,或者是結構體中的 mutating 方法。

in-out 參數沖突訪問

函數會對它所有的 in-out 參數進行長期寫訪問。in-out 參數的寫訪問會在所有非 in-out 參數處理完成之后開始,直到函數執行完畢為止。如果有多個 in-out 參數,則寫訪問開始的順序與參數的順序一致。

不能在訪問以 in-out 形式傳入后的原始變量。

var stepSize = 1
func increment(_ number: inout Int) {
    number += stepSize // 訪問原始變量
}
increment(&stepSize)
// 錯誤:stepSize 訪問沖突

沖突的原因是 stepSize 的讀訪問與 number 的寫訪問重疊了。解決的方案就是拷貝一份地址的索引。

// 顯式拷貝
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// 更新原來的值
stepSize = copyOfStepSize
// stepSize 現在的值是 2

另外,在多個 in-out 參數時傳入同一個變量同樣容易發生沖突。

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 訪問沖突

更多的例子。

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}
extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}
oscar.shareHealth(with: &oscar)
// 錯誤:oscar 訪問沖突

因為 Player 是結構體,屬于值類型,修改值的任何一個部分都是對于整個值的修改,這意味著其中的一個屬性的讀寫訪問都需要訪問整個值。和結構體類似的值類型還有 元組、枚舉類型。

下面的代碼展示了一樣的錯誤,對于一個存儲在全局變量里的結構體屬性的寫訪問重疊了。

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // 錯誤

將全局變量改為本地變量,編譯器就可以保證重疊訪問是安全的。

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // 正常
}

遵循以下原則,可以保證結構體屬性的重疊反問安全。

  • 訪問的實例是存儲屬性,而不是計算屬性或類型屬性
  • 結構體是本地變量的值,而不是全局變量
  • 結構體只能被非逃逸閉包捕獲

訪問控制

模塊和源文件

模塊:每個 target 都會被看作獨立的模塊。如框架和應用程序
源文件:模塊中的源代碼文件

訪問級別

  • open 和 public
    開放級別:包內包外都可以訪問。如框架中的 api。

  • internal
    包內級別:只能被包內訪問。一般情況下的默認訪問級別。

  • fileprivate
    文件級別:只能在定義的源文件內部訪問。

  • private
    私有級別:只能在定義的作用域訪問。

open 針對類和類的成員,它和 public 的區別主要在于 open 限定的類和成員能夠在模塊之外被繼承和重寫。

基本原則

實體不能定義在具有更低級別的實體中。

例如,函數的訪問級別不能高于它的參數類型和返回類型的訪問級別。

實體的訪問級別決定了實體中的所有成員變量的上限。

public class SomePublicClass {                  // 顯式 public 類
    public var somePublicProperty = 0            // 顯式 public 類成員
    var someInternalProperty = 0                 // 隱式 internal 類成員
    fileprivate func someFilePrivateMethod() {}  // 顯式 fileprivate 類成員
    private func somePrivateMethod() {}          // 顯式 private 類成員
}

class SomeInternalClass {                       // 隱式 internal 類
    var someInternalProperty = 0                 // 隱式 internal 類成員
    fileprivate func someFilePrivateMethod() {}  // 顯式 fileprivate 類成員
    private func somePrivateMethod() {}          // 顯式 private 類成員
}

fileprivate class SomeFilePrivateClass {        // 顯式 fileprivate 類
    func someFilePrivateMethod() {}              // 隱式 fileprivate 類成員
    private func somePrivateMethod() {}          // 顯式 private 類成員
}

private class SomePrivateClass {                // 顯式 private 類
    func somePrivateMethod() {}                  // 隱式 private 類成員
}
  • 元組類型

元組的訪問級別由元組中訪問級別最低的類型決定,不能被顯示指定。

  • 函數類型

函數的訪問級別根據訪問級別最低的參數類型或者返回類型來決定。

  • 枚舉類型

枚舉成員的訪問級別和該枚舉類型相同,不能單獨指定枚舉成員的訪問級別。
原始值或者關聯值的訪問級別不能低于枚舉類型的訪問級別。

  • 嵌套類型

嵌套類型的訪問級別和包含它的類型的訪問級別相同。但是 public 除外,在一個 public 的類型中定義嵌套類型,該嵌套類型默認情況下自動擁有 internale 的訪問級別,你可以顯示指定嵌套類型訪問級別為 public 。

  • 子類

子類的訪問級別不能高于父類的訪問級別。
但是你可以通過重寫給子類的成員提供更高的訪問級別。

public class A {
    fileprivate func someMethod() {}
}
internal class B: A {
    // 重寫更改方法的訪問級別
    override internal func someMethod() {}
}
  • 常量、變量、屬性、下標

常量、變量、屬性不能擁有比它們類型更高的訪問級別,下標也不能擁有比索引類型或返回類型更高的訪問級別。

private var privateInstance = SomePrivateClass()

SomePrivateClass 是私有類,所以變量 privateInstance 必須明確指定訪問級別為私有。

Getter 和 Setter

常量、變量、屬性、下標的 Getters 和 Setters 的訪問級別和它們所屬類型的訪問級別相同。但是 Setter 可以擁有更低的訪問級別,這樣可以控制讀寫權限。

下面例子限制了結構體中 numberOfEdits 寫的訪問級別,只讀。

struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}
  • 構造器

必要構造器的訪問級別和所屬類型的訪問級別相同。便利構造器的訪問級別則不能高于所屬類型的訪問級別,因為便利構造器需要橫向使用到必要構造器。

  • 協議

協議中的每個方法或屬性都必須具有和該協議相同的訪問級別,不能將協議中的方法或屬性設置為其他訪問級別。

和其他類型不同,具有 public 訪問級別的協議,其成員也都是 public。而其他類型,如類,訪問級別為 public ,其成員的訪問級別除非你顯示指定,否則默認都是 internal。

協議繼承

繼承來的新協議只能擁有比原始協議更低的訪問級別。

協議遵循

一個類型可以遵循比它訪問級別更低的協議,但是所遵循的上下文級別取類型和協議級別最小的那個。如果一個類型是 public 級別,但它要遵循的協議是 internal 級別,那么這個類型對該協議的遵循上下文就是 internal 級別。

當你編寫或擴展一個類型讓它遵循一個協議時,你必須確保該類型對協議的每一個要求的實現,至少與遵循協議的上下文級別一致。例如,一個 public 類型遵循一個 internal 協議,這個類型對協議的所有實現至少都應是 internal 級別的。

  • Extension

Extension 的新增成員具有和原始類型成員一致的訪問級別。當然你可以顯示為新增成員指定更低的級別。

在同一文件類,原始類型聲明中的私有成員,可以在多個 extension 中訪問,extension 中私有成員同樣可以在另一個 extension 或原始類型聲明中訪問。因此,你可以通過 extension 來任意組織你的代碼。這一點比 OC 方便得多。

  • 泛型

泛型類型或者泛型函數的訪問級別取決于泛型類型或者泛型函數本身的訪問級別,另外還需要結合類型參數的類型約束的訪問級別,選擇最小的訪問級別。

高級運算符

  • 位運算符

~ :按位取反,比特位取反。
& :按位與,兩個數對應位都為 1 時,新數的對應位才為 1。
| :按位或,兩個數對應位中有任意一個為 1 時,新數的對應位就為 1。
^ :按位異或,當兩個數對應位不相同時,新數的對應位就為 1,并且對應位相同時則為 0。

  • 溢出運算符

&+ :溢出加法
&- :溢出減法
&* :溢出乘法

  • 運算符重載

類和結構體可以為現有的運算符提供自定義的實現,以達到類和結構體進行運算。

struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func + (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y + right.y)
    }
}

let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// negative 是一個值為 (-3.0, -4.0) 的 Vector2D 實例
let alsoPositive = -negative
// alsoPositive 是一個值為 (3.0, 4.0) 的 Vector2D 實例

前后綴運算符

要實現前綴或者后綴運算符,需要在聲明運算符函數的時候在 func 關鍵字之前指定 prefix 或者 postfix 修飾符。

extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}

復合賦值運算符

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right
    }
}

var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original 的值現在為 (4.0, 6.0)

等價運算符

通常情況下,自定義的類和結構體沒有對等價運算符進行默認實現,等價運算符通常被稱為相等運算符(==)與不等運算符(!=)。

與其它中綴運算符一樣, 并且增加對標準庫 Equatable 協議的遵循。

extension Vector2D: Equatable {
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }
}

let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
    print("These two vectors are equivalent.")
}
// 打印“These two vectors are equivalent.”

多數簡單情況下,您可以使用 Swift 為您提供的等價運算符默認實現。Swift 為以下數種自定義類型提供等價運算符的默認實現:

  1. 只擁有存儲屬性,并且它們全都遵循 Equatable 協議的結構體
  2. 只擁有關聯類型,并且它們全都遵循 Equatable 協議的枚舉
  3. 沒有關聯類型的枚舉

例如:

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}
  • 自定義運算符

新的運算符要使用 operator 關鍵字在全局作用域內進行定義,同時還要指定 prefixinfix 或者 postfix 修飾符。

例如:

prefix operator +++
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容