天天看點

Swift底層原理探索----屬性 & 方法屬性方法下标

目錄

【傳回目錄】

Swift底層原理探索----屬性 & 方法

  • 屬性
    • 存儲屬性
    • 計算屬性
    • 枚舉rawValue原理
    • 延遲存儲屬性(Lazy Stored Property)
      • 延遲存儲屬性注意點
    • 屬性觀察器(Property Observer)
      • 全局變量、局部變量
    • `inout`的再次研究
    • `inout`的本質總結
    • 類型屬性(Type Property)
    • 類型屬性細節
    • 單例模式
    • 類型(static)存儲屬性的本質
  • 方法
    • 方法
    • mutating
    • @discardableResult
  • 下标
    • 下标的細節
    • 結構體、類作為傳回值的對比
    • 下标接受多個參數

屬性

struct Circle {
    //存儲屬性
    var radius: Double
    //計算屬性
    var diamiter: Double {
        set { 
            radius = newValue / 2
        }
        get {
            radius * 2
        }
    }
}
           
  • Swift中跟執行個體相關的屬性可以分為2大類
    • 存儲屬性(

      Stored Property

      • 類似于成員變量這個概念
      • 存儲在執行個體的記憶體中
        Swift底層原理探索----屬性 & 方法屬性方法下标
      • 結構體、類可以定義存儲屬性
        Swift底層原理探索----屬性 & 方法屬性方法下标
      • 枚舉

        不可以

        定義存儲屬性
        Swift底層原理探索----屬性 & 方法屬性方法下标
        我們知道枚舉的記憶體裡面可以存放的是所有的

        case

        以及

        關聯值

        ,并沒有所謂的成員變量概念,可是以也不存在所謂的存儲屬性
    • 計算屬性(

      Computed Property

      • 本質就是方法(函數)這個也可以通過彙編來證明一下
        Swift底層原理探索----屬性 & 方法屬性方法下标
        Swift底層原理探索----屬性 & 方法屬性方法下标
        Swift底層原理探索----屬性 & 方法屬性方法下标
        Swift底層原理探索----屬性 & 方法屬性方法下标
      • 不占用執行個體的記憶體
        Swift底層原理探索----屬性 & 方法屬性方法下标
      • 枚舉、結構體、類都可以定義計算屬性

存儲屬性

  • 關于存儲屬性,

    Swift

    有個明确的規定
    • 在建立類 或 結構體的時候,必須為所有的存儲屬性設定一個合适的初始值,也就是要求類/結構體建立執行個體後,它的全部記憶體要得到初始化,而存儲屬性正好就是放在執行個體的記憶體裡面的,是以需要将所有的存儲屬性設定初始值。
      1. 可以在初始化器裡為存儲屬性設定一個初始值
        Swift底層原理探索----屬性 & 方法屬性方法下标
        Swift底層原理探索----屬性 & 方法屬性方法下标
      2. 可以配置設定一個預設的屬性值作為屬性定義的一部分
        Swift底層原理探索----屬性 & 方法屬性方法下标

計算屬性

  • set

    傳入的新值預設叫做

    newValue

    ,也可以自定義
  • 定義計算屬性隻能用

    var

    , 不能用

    let

    • let

      代表常量,也就是值是一成不變的
    • 計算屬性的值是可能發生變化的(即使是隻讀計算屬性)
  • 隻讀計算屬性:隻有

    get

    , 沒有

    set

枚舉rawValue原理

  • 枚舉原始值

    rawValue

    的本質是:隻讀計算屬性,直接看彙編就可以證明
    Swift底層原理探索----屬性 & 方法屬性方法下标
    Swift底層原理探索----屬性 & 方法屬性方法下标
    Swift底層原理探索----屬性 & 方法屬性方法下标

延遲存儲屬性(Lazy Stored Property)

看現這段代碼

class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is running!")
    }
}

class Person {
    var car = Car()
    init() {
        print("Person init")
    }
    func  goOut() {
        car.run()
    }
}

let p = Person()
print("-----------")
p.goOut()
           

運作結果如下

Car init
Person init
-----------
Car is running!
Program ended with exit code: 0
           

我們給上面代碼的car屬性增加一個關鍵字

lazy

修飾

class Car {
    init() {
        print("Car init")
    }
    func run() {
        print("Car is running!")
    }
}

class Person {
    lazy var car = Car()
    init() {
        print("Person init")
    }
    func  goOut() {
        car.run()
    }
}

let p = Person()
print("-----------")
p.goOut()
           

再看下現在的運作結果

Person init
-----------
Car init
Car is running!
Program ended with exit code: 0
           

可以看出,

lazy

的作用,是将屬性

var car

的初始化延遲到了它首次使用的時候進行,例子中也就是

p.goOut()

這句代碼執行的時候,才回去初始化屬性

car

通過lazy 關鍵字修飾的存儲屬性就要做

延遲存儲屬性

,這個功能的好處是顯而易見的,因為有些屬性可能需要花費很多資源進行初始化,而很可能在某些極少情況下才會被觸發使用,是以

lazy

關鍵字就可以用在這種情況下,讓核心對象的初始化變得快速而輕量。比如下面這個例子

class PhotoView {
    lazy var image: Image = {
        let url = "https://www.520it.com/xx.png"
        let data = Data(url: url)
        return Image(dada: data)
    }()
}
           

網絡圖檔的加載往往是需要一些時間的,上面例子裡面圖檔的加載過程封裝在閉包表達式裡面,并且将其傳回值作為了

image

屬性的初始化指派,通過

lazy

,就講這個加載的過程推遲到了

image

在實際被用到的時候去執行,這樣就可以提升app順滑度,改善卡頓情況。

  • 使用

    lazy

    可以定義一個延遲存儲屬性,在第一次用到屬性的時候才會進行初始化
  • lazy

    屬性必須是

    var

    , 不能是

    let

    • 這個要求很容易了解,

      let

      必須在

      執行個體

      的初始化方法完成之前就擁有值,而

      lazy

      恰好是為了在執行個體建立并初始化之後的某個時刻對其某個屬性進行初始化指派,是以

      lazy

      隻能作用域

      var

      屬性
  • 如果多線程同時第一次通路

    lazy

    屬性,無法保證屬性隻被初始化

    1

延遲存儲屬性注意點

  • 當結構體包含一個延遲存儲屬性時,隻有

    var

    才能通路延遲存儲屬性

    因為延遲屬性初始化時需要改變結構體的記憶體

    Swift底層原理探索----屬性 & 方法屬性方法下标
    案例中,因為

    p

    是常量,是以記憶體的内容初始化之後不可以變化,但是p.z會使得結構體

    Point

    lazy var z

    屬性進行初始化,因為結構體的成員是在結構體的記憶體裡面的,是以就需要改變結構體的記憶體,是以便産生了後面的報錯。

屬性觀察器(Property Observer)

  • 可以為

    非lazy

    var

    存儲屬性設定屬性觀察器
  • willSet

    會傳遞新值,預設叫做

    newValue

  • didSet

    會傳遞舊值,預設叫做

    oldValue

  • 在初始化器中設定屬性值不會出發

    willSet

    didSet

  • 在屬性定義時設定初始值也不會出發

    willSet

    didSet

struct Circle {
    var radius: Double {
        willSet {
            print("willSet", newValue)
        }

        didSet {
            print("didSet", oldValue, radius)
        }
    }

    init() {
        self.radius = 1.0
        print("Circle init!")
    }
}

var circle = Circle()
circle.radius = 10.5
print(circle.radius)
           

運作結果

Circle init!
willSet 10.5
didSet 1.0 10.5
10.5
Program ended with exit code: 0
           

全局變量、局部變量

屬性觀察器、計算屬性的功能,同樣可以應用在全局變量、局部變量身上
var num: Int {
   get {
       return 10
   }
   set {
       print("setNum", newValue)
   }
}
num = 12
print(num)


func test() {
   var age = 10 {
       willSet {
           print("willSet", newValue)
       }
       didSet {
           print("didSet", oldValue, age)
       }
   }

   age = 11
}
test()

           

inout

的再次研究

首先看下面的代碼

func test(_ num: inout Int) {
    num = 20
}

var age = 10
test(&age) // 此處加斷點
           

将程式運作至斷點處,觀察彙編

SwiftTest`main:
    0x1000010b0 <+0>:  pushq  %rbp
    0x1000010b1 <+1>:  movq   %rsp, %rbp
    0x1000010b4 <+4>:  subq   $0x30, %rsp
    0x1000010b8 <+8>:  leaq   0x6131(%rip), %rax        ; SwiftTest.age : Swift.Int
    0x1000010bf <+15>: xorl   %ecx, %ecx
    0x1000010c1 <+17>: movq   $0xa, 0x6124(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x1000010cc <+28>: movl   %edi, -0x1c(%rbp)
->  0x1000010cf <+31>: movq   %rax, %rdi
    0x1000010d2 <+34>: leaq   -0x18(%rbp), %rax
    0x1000010d6 <+38>: movq   %rsi, -0x28(%rbp)
    0x1000010da <+42>: movq   %rax, %rsi
    0x1000010dd <+45>: movl   $0x21, %edx
    0x1000010e2 <+50>: callq  0x10000547c               ; symbol stub for: swift_beginAccess
    0x1000010e7 <+55>: leaq   0x6102(%rip), %rdi        ; SwiftTest.age : Swift.Int
    0x1000010ee <+62>: callq  0x100001110               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x1000010f3 <+67>: leaq   -0x18(%rbp), %rdi
    0x1000010f7 <+71>: callq  0x10000549a               ; symbol stub for: swift_endAccess
    0x1000010fc <+76>: xorl   %eax, %eax
    0x1000010fe <+78>: addq   $0x30, %rsp
    0x100001102 <+82>: popq   %rbp
    0x100001103 <+83>: retq  
           

我們可以看到函數

test

調用之前,參數的傳遞情況如下

Swift底層原理探索----屬性 &amp; 方法屬性方法下标

對于上述比較簡單的情況,我們知道

inout

的本質就是進行引用傳遞,接下來,我們考慮一些更加複雜的情況

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}



var s = Shape(width: 10, side: 4)
test(&s.width)	// 斷點1
s.show()
print("-------------")
test(&s.side)   //斷點2
s.show()
print("-------------")
test(&s.girth)  //斷點3
s.show()
print("-------------")
           

上述案例裡面,全局變量s的類型是結構體

Struct Shape

,它的記憶體放的是兩個存儲屬性

width

side

,其中

side

帶有屬性觀察器,另外Shape還有一個計算屬性

girth

,我們首先不加斷點運作一下程式,觀察一下運作結果

getGirth
width= 20, side= 4, girth= 80
-------------
willSetSide 20
didSetSide 4 20
getGirth
width= 20, side= 20, girth= 400
-------------
getGirth
setGirth 20
getGirth
width= 1, side= 20, girth= 20
-------------
Program ended with exit code: 0
           

看得出來,

inout

對于三種屬性都産生了作用,那麼它的底層到底是如何處理和實作的呢?我們還是要通過彙編來一探究竟。便于彙編分析,我們截取部分代碼進行編譯運作

首先看

普通的屬性

👇👇👇👇
struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.width) // 斷點處,傳入普通屬性width作為test的inout參數
           

彙編結果如下

SwiftTest`main:
    0x100001310 <+0>:   pushq  %rbp
    0x100001311 <+1>:   movq   %rsp, %rbp
    0x100001314 <+4>:   subq   $0x30, %rsp
    0x100001318 <+8>:   movl   $0xa, %eax
    0x10000131d <+13>:  movl   %edi, -0x1c(%rbp)
    0x100001320 <+16>:  movq   %rax, %rdi
    0x100001323 <+19>:  movl   $0x4, %eax
    0x100001328 <+24>:  movq   %rsi, -0x28(%rbp)
    0x10000132c <+28>:  movq   %rax, %rsi
    0x10000132f <+31>:  callq  0x100001d60               ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
    0x100001334 <+36>:  leaq   0x6ebd(%rip), %rcx        ; SwiftTest.s : SwiftTest.Shape
    0x10000133b <+43>:  xorl   %r8d, %r8d
    0x10000133e <+46>:  movl   %r8d, %esi
    0x100001341 <+49>:  movq   %rax, 0x6eb0(%rip)        ; SwiftTest.s : SwiftTest.Shape
    0x100001348 <+56>:  movq   %rdx, 0x6eb1(%rip)        ; SwiftTest.s : SwiftTest.Shape + 8
->  0x10000134f <+63>:  movq   %rcx, %rdi
    0x100001352 <+66>:  leaq   -0x18(%rbp), %rax
    0x100001356 <+70>:  movq   %rsi, -0x30(%rbp)
    0x10000135a <+74>:  movq   %rax, %rsi
    0x10000135d <+77>:  movl   $0x21, %edx
    0x100001362 <+82>:  movq   -0x30(%rbp), %rcx
    0x100001366 <+86>:  callq  0x100006312               ; symbol stub for: swift_beginAccess
    0x10000136b <+91>:  leaq   0x6e86(%rip), %rdi        ; SwiftTest.s : SwiftTest.Shape
    0x100001372 <+98>:  callq  0x100001d70               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x100001377 <+103>: leaq   -0x18(%rbp), %rdi
    0x10000137b <+107>: callq  0x100006330               ; symbol stub for: swift_endAccess
    0x100001380 <+112>: xorl   %eax, %eax
    0x100001382 <+114>: addq   $0x30, %rsp
    0x100001386 <+118>: popq   %rbp
    0x100001387 <+119>: retq
           

參數傳遞流程如下圖

Swift底層原理探索----屬性 &amp; 方法屬性方法下标

是以對于

普通的存儲屬性

test

函數是直接将它的位址值傳入。

接下來便于直覺的對比,我們再看一下

計算屬性

的情況👇👇👇👇
struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}

func test(_ num: inout Int) {
	print("開始test函數")
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.girth)
           

斷點處彙編如下

SwiftTest`main:
    0x1000012f0 <+0>:   pushq  %rbp
    0x1000012f1 <+1>:   movq   %rsp, %rbp
    0x1000012f4 <+4>:   pushq  %r13
    0x1000012f6 <+6>:   subq   $0x38, %rsp
    0x1000012fa <+10>:  movl   $0xa, %eax
    0x1000012ff <+15>:  movl   %edi, -0x2c(%rbp)
    0x100001302 <+18>:  movq   %rax, %rdi
    0x100001305 <+21>:  movl   $0x4, %eax
    0x10000130a <+26>:  movq   %rsi, -0x38(%rbp)
    0x10000130e <+30>:  movq   %rax, %rsi
    0x100001311 <+33>:  callq  0x100001d60               ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
    0x100001316 <+38>:  leaq   0x6edb(%rip), %rcx        ; SwiftTest.s : SwiftTest.Shape
    0x10000131d <+45>:  xorl   %r8d, %r8d
    0x100001320 <+48>:  movl   %r8d, %esi
    0x100001323 <+51>:  movq   %rax, 0x6ece(%rip)        ; SwiftTest.s : SwiftTest.Shape
    0x10000132a <+58>:  movq   %rdx, 0x6ecf(%rip)        ; SwiftTest.s : SwiftTest.Shape + 8
->  0x100001331 <+65>:  movq   %rcx, %rdi
    0x100001334 <+68>:  leaq   -0x20(%rbp), %rax
    0x100001338 <+72>:  movq   %rsi, -0x40(%rbp)
    0x10000133c <+76>:  movq   %rax, %rsi
    0x10000133f <+79>:  movl   $0x21, %edx
    0x100001344 <+84>:  movq   -0x40(%rbp), %rcx
    0x100001348 <+88>:  callq  0x100006312               ; symbol stub for: swift_beginAccess
    0x10000134d <+93>:  movq   0x6ea4(%rip), %rdi        ; SwiftTest.s : SwiftTest.Shape
    0x100001354 <+100>: movq   0x6ea5(%rip), %rsi        ; SwiftTest.s : SwiftTest.Shape + 8
    0x10000135b <+107>: callq  0x1000016d0               ; SwiftTest.Shape.girth.getter : Swift.Int at main.swift:646
    0x100001360 <+112>: movq   %rax, -0x28(%rbp)
    0x100001364 <+116>: leaq   -0x28(%rbp), %rdi
    0x100001368 <+120>: callq  0x100001d70               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x10000136d <+125>: movq   -0x28(%rbp), %rdi
    0x100001371 <+129>: leaq   0x6e80(%rip), %r13        ; SwiftTest.s : SwiftTest.Shape
    0x100001378 <+136>: callq  0x100001820               ; SwiftTest.Shape.girth.setter : Swift.Int at main.swift:642
    0x10000137d <+141>: leaq   -0x20(%rbp), %rdi
    0x100001381 <+145>: callq  0x100006330               ; symbol stub for: swift_endAccess
    0x100001386 <+150>: xorl   %eax, %eax
    0x100001388 <+152>: addq   $0x38, %rsp
    0x10000138c <+156>: popq   %r13
    0x10000138e <+158>: popq   %rbp
    0x10000138f <+159>: retq 
           

這一次從彙編代碼量就可以判斷,對于計算屬性的處理肯定比存儲屬性要複雜,還是通過圖例來展示一下整個過程

Swift底層原理探索----屬性 &amp; 方法屬性方法下标
Swift底層原理探索----屬性 &amp; 方法屬性方法下标

可以看出,由于計算屬性在執行個體内部沒有對應的記憶體空間,編譯器通過在函數棧裡面開辟一個局部變量的方法,利用它作為計算屬性的值的臨時宿主,并且将該局部變量的位址作為

test

函數的

inout

參數傳入函數,是以本質上,仍然是

引用傳遞

test

函數調用前,計算屬性值給複制到局部變量上,以及

test

函數調用之後,局部變量的值傳遞給setter函數的這兩個過程,被蘋果成為 Copy In Copy Out,上面案例代碼的運作結果也驗證了這個結論

getGirth
開始test函數
setGirth 20
Program ended with exit code: 0
           
最後,我們來看對于

帶有屬性觀察器的存儲屬性

,處理過程會有哪些獨到之處👇👇👇👇
struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }

    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }

    func show() {
        print("width= \(width), side= \(side), girth= \(girth)")
    }
}


func test(_ num: inout Int) {
    num = 20
}

var s = Shape(width: 10, side: 4)
test(&s.side) //side是帶屬性觀察期的存儲屬性, 斷點在這裡
           

斷點處彙編結果如下

SwiftTest`main:
    0x100001230 <+0>:   pushq  %rbp
    0x100001231 <+1>:   movq   %rsp, %rbp
    0x100001234 <+4>:   pushq  %r13
    0x100001236 <+6>:   subq   $0x38, %rsp
    0x10000123a <+10>:  movl   $0xa, %eax
    0x10000123f <+15>:  movl   %edi, -0x2c(%rbp)
    0x100001242 <+18>:  movq   %rax, %rdi
    0x100001245 <+21>:  movl   $0x4, %eax
    0x10000124a <+26>:  movq   %rsi, -0x38(%rbp)
    0x10000124e <+30>:  movq   %rax, %rsi
    0x100001251 <+33>:  callq  0x100001ca0               ; SwiftTest.Shape.init(width: Swift.Int, side: Swift.Int) -> SwiftTest.Shape at main.swift:630
    0x100001256 <+38>:  leaq   0x6f9b(%rip), %rcx        ; SwiftTest.s : SwiftTest.Shape
    0x10000125d <+45>:  xorl   %r8d, %r8d
    0x100001260 <+48>:  movl   %r8d, %esi
    0x100001263 <+51>:  movq   %rax, 0x6f8e(%rip)        ; SwiftTest.s : SwiftTest.Shape
    0x10000126a <+58>:  movq   %rdx, 0x6f8f(%rip)        ; SwiftTest.s : SwiftTest.Shape + 8
->  0x100001271 <+65>:  movq   %rcx, %rdi
    0x100001274 <+68>:  leaq   -0x20(%rbp), %rax
    0x100001278 <+72>:  movq   %rsi, -0x40(%rbp)
    0x10000127c <+76>:  movq   %rax, %rsi
    0x10000127f <+79>:  movl   $0x21, %edx
    0x100001284 <+84>:  movq   -0x40(%rbp), %rcx
    0x100001288 <+88>:  callq  0x100006302               ; symbol stub for: swift_beginAccess
    0x10000128d <+93>:  movq   0x6f6c(%rip), %rax        ; SwiftTest.s : SwiftTest.Shape + 8
    0x100001294 <+100>: movq   %rax, -0x28(%rbp)
    0x100001298 <+104>: leaq   -0x28(%rbp), %rdi
    0x10000129c <+108>: callq  0x100001cb0               ; SwiftTest.test(inout Swift.Int) -> () at main.swift:658
    0x1000012a1 <+113>: movq   -0x28(%rbp), %rdi
    0x1000012a5 <+117>: leaq   0x6f4c(%rip), %r13        ; SwiftTest.s : SwiftTest.Shape
    0x1000012ac <+124>: callq  0x100001350               ; SwiftTest.Shape.side.setter : Swift.Int at main.swift:632
    0x1000012b1 <+129>: leaq   -0x20(%rbp), %rdi
    0x1000012b5 <+133>: callq  0x100006320               ; symbol stub for: swift_endAccess
    0x1000012ba <+138>: xorl   %eax, %eax
    0x1000012bc <+140>: addq   $0x38, %rsp
    0x1000012c0 <+144>: popq   %r13
    0x1000012c2 <+146>: popq   %rbp
    0x1000012c3 <+147>: retq   
           
Swift底層原理探索----屬性 &amp; 方法屬性方法下标

這次,我們發現跟計算屬性有些類似,這裡也用到了函數棧的局部變量,它的作用是用來承載計算屬性的值,然後被傳入test函數的同樣是這個局部變量的位址(引用),但是我很好奇為何要多此一舉,計算屬性因為本身沒有固定的記憶體,是以很好了解必須借助局部變臉作為臨時宿主,但是計算屬性是有固定記憶體的,可以猜的到,這麼設計的原因肯定跟屬性觀察器有關,但是目前的代碼還不足以解釋這麼設計的意圖,但是我們看到這裡最後一步,調用了side.setter函數,🤔️side是存儲屬性,怎麼會有setter函數呢?那我們就進入它内部看看喽,它的彙編如下

SwiftTest`Shape.side.setter:
->  0x100001350 <+0>:  pushq  %rbp
    0x100001351 <+1>:  movq   %rsp, %rbp
    0x100001354 <+4>:  pushq  %r13
    0x100001356 <+6>:  subq   $0x28, %rsp
    0x10000135a <+10>: movq   $0x0, -0x10(%rbp)
    0x100001362 <+18>: movq   $0x0, -0x18(%rbp)
    0x10000136a <+26>: movq   %rdi, -0x10(%rbp)
    0x10000136e <+30>: movq   %r13, -0x18(%rbp)
    0x100001372 <+34>: movq   0x8(%r13), %rax
    0x100001376 <+38>: movq   %rax, %rcx
    0x100001379 <+41>: movq   %rdi, -0x20(%rbp)
    0x10000137d <+45>: movq   %r13, -0x28(%rbp)
    0x100001381 <+49>: movq   %rax, -0x30(%rbp)
    0x100001385 <+53>: callq  0x1000013b0               ; SwiftTest.Shape.side.willset : Swift.Int at main.swift:633
    0x10000138a <+58>: movq   -0x28(%rbp), %rax
    0x10000138e <+62>: movq   -0x20(%rbp), %rcx
    0x100001392 <+66>: movq   %rcx, 0x8(%rax)
    0x100001396 <+70>: movq   -0x30(%rbp), %rdi
    0x10000139a <+74>: movq   %rax, %r13
    0x10000139d <+77>: callq  0x1000014d0               ; SwiftTest.Shape.side.didset : Swift.Int at main.swift:636
    0x1000013a2 <+82>: movq   -0x30(%rbp), %rax
    0x1000013a6 <+86>: addq   $0x28, %rsp
    0x1000013aa <+90>: popq   %r13
    0x1000013ac <+92>: popq   %rbp
    0x1000013ad <+93>: retq 
           
Swift底層原理探索----屬性 &amp; 方法屬性方法下标

原來,這個

side

的兩個屬性觀察器

willSet

didSet

被包裹在了這個

setter

函數裡面,而且,對于屬性

side

的指派真正發生在這個

setter

函數裡面。

是以我們看出了一個細節,屬性

side

記憶體裡的值被修改的時間點,是在

test

函數之後,也就是這個

setter

函數裡,也就是

test

函數其實并沒有修改

side

的值。

因為

test

函數的功能拿到一段記憶體,并且修改裡面的值,如果目前我們将

side

的位址送出給

test

,除了能夠修改

side

記憶體裡值以外,它是無法觸發

side

的屬性觀察器的。是以看得出局部變量以及

setter

函數出現在這裡的意義就是為了能夠去觸發屬性

side

的屬性觀察器。因為我們使用了局部變量,是以對于帶有屬性觀察器的存儲屬性,也可以說inout對其采用了

Copy In Copy Out

的做法。

通過程式運作之後的輸出結果,也可以驗證我們已上的結論

開始test函數
willSetSide 20
didSetSide 4 20
Program ended with exit code: 0
           

inout

的本質總結

  • 如果實參有實體記憶體位址,且沒有設定屬性觀察器

    則直接将實參的記憶體位址傳入函數(

    實參進行引用傳遞

  • 如果實參是計算屬性 或者 設定了屬性觀察器

    則采取了 Copy In Copy Out的做法

    • 調用該函數時,先複制實參的值,産生副本【可以了解成

      get

      操作】
    • 将副本的記憶體位址傳入函數(

      副本進行引用傳遞

      ),在函數内部可以修改副本的值
    • 函數傳回後,再将副本的值覆寫實參的值【可以了解成

      set

      操作】
總結:

inout

的本質就是

引用傳遞

(位址傳遞)

類型屬性(Type Property)

  • 嚴格來說,屬性可以劃分為:
    • 執行個體屬性(Instance Property):隻能通過執行個體去通路
      • 存儲執行個體屬性(Stored Instance Property):存儲在執行個體的記憶體中,每個執行個體都有一份
      • 計算執行個體屬性(Computed Instance Property):
    • 類型屬性(Type Property):隻能通過類型去通路
      • 存儲類型屬性(Stored Type Property):整個程式的運作過程中,就隻有一份記憶體,它的本質就是全局變量
      • 計算類型屬性(Computed Type Property)
  • 可以通過

    static

    定義類型屬性,對于類來說,還可以用關鍵字

    class

類型屬性細節

  • 不同于存儲執行個體屬性,你必須給存儲類型屬性設定初始值

    因為類型沒有像執行個體那樣的

    init

    初始化器來初始化存儲屬性
    Swift底層原理探索----屬性 &amp; 方法屬性方法下标
  • 存儲類型屬性預設就是

    lazy

    , 會在第一次使用的時候才初始化
    • 就算被多個線程同時通路,保證隻會初始化一次,可以保證線程安全(系統底層會有加鎖處理)
    • 存儲類型屬性可以時

      let

      ,因為這裡壓根不存在執行個體初始化的過程
  • 枚舉類型也可以定義類型屬性(存儲類型屬性、計算類型屬性)

單例模式

public class FileManager {
    
    public static let shared = FileManager()
    
    private init(){
        
    }
}
           
  • public static let shared = FileManager()

    • 通過

      static

      定義了一個類型存儲屬性,
    • public

      確定在任何場景下,外界都能通路,
    • let

      保證了

      FileManager()

      隻會被指派給

      shared

      一次,并且確定了線程安全,也就是說

      init()

      方法隻會被調用一次,這樣就確定

      FileManager

      隻會存在唯一一個執行個體,這就是Swift中的單例。
  • private init()

    private

    確定了外界是無法手動調用

    FileManager()

    來建立執行個體,是以通過

    shared

    屬性得到的

    FileManager

    執行個體永遠是相同的一份,這也符合了我們對與單例的要求。

類型(static)存儲屬性的本質

前面我們介紹static存儲屬性的時候,提到了它實際上是全局變量,現在來證明一下,首先我們看看普通的全局變量是怎麼樣的

var num1 = 10 // 此處加斷點
var num2 = 11
var num3 = 12
           

運作至斷點處,彙編如下

SwiftTest`main:
    0x100001120 <+0>:  pushq  %rbp
    0x100001121 <+1>:  movq   %rsp, %rbp
    0x100001124 <+4>:  xorl   %eax, %eax
->  0x100001126 <+6>:  movq   $0xa, 0x60af(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100001131 <+17>: movq   $0xb, 0x60ac(%rip)        ; SwiftTest.num1 : Swift.Int + 4
    0x10000113c <+28>: movq   $0xc, 0x60a9(%rip)        ; SwiftTest.num2 : Swift.Int + 4
    0x100001147 <+39>: popq   %rbp
    0x100001148 <+40>: retq
           

很明顯,下圖的這三句分别對應的就是

num1

num2

num3

Swift底層原理探索----屬性 &amp; 方法屬性方法下标

我們來算一下他們的實際記憶體位址

  • &num1 = 0x60af + 0x100001131 = 0x1000071E0

  • &num2 = 0x60ac + 0x10000113c = 0x1000071E8

  • &num3 = 0x60a9 + 0x100001147 = 0x1000071F0

它們就是全局資料段上的3段連續記憶體空間。接下來我們加入static存儲屬性如下

var num1 = 10 // 斷點處

class Car {
    static var num2 = 1
}

Car.num2 = 11

var num3 = 12
           

打開斷點處的彙編

SwiftTest`main:
    0x100000d80 <+0>:  pushq  %rbp
    0x100000d81 <+1>:  movq   %rsp, %rbp
    0x100000d84 <+4>:  subq   $0x30, %rsp
->  0x100000d88 <+8>:  movq   $0xa, 0x6595(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100000d93 <+19>: movl   %edi, -0x1c(%rbp)
    0x100000d96 <+22>: movq   %rsi, -0x28(%rbp)
    0x100000d9a <+26>: callq  0x100000e40               ; SwiftTest.Car.num2.unsafeMutableAddressor : Swift.Int at main.swift
    0x100000d9f <+31>: xorl   %ecx, %ecx
    0x100000da1 <+33>: movq   %rax, %rdx
    0x100000da4 <+36>: movq   %rdx, %rdi
    0x100000da7 <+39>: leaq   -0x18(%rbp), %rsi
    0x100000dab <+43>: movl   $0x21, %edx
    0x100000db0 <+48>: movq   %rax, -0x30(%rbp)
    0x100000db4 <+52>: callq  0x1000053a2               ; symbol stub for: swift_beginAccess
    0x100000db9 <+57>: movq   -0x30(%rbp), %rax
    0x100000dbd <+61>: movq   $0xb, (%rax)
    0x100000dc4 <+68>: leaq   -0x18(%rbp), %rdi
    0x100000dc8 <+72>: callq  0x1000053c6               ; symbol stub for: swift_endAccess
    0x100000dcd <+77>: xorl   %eax, %eax
    0x100000dcf <+79>: movq   $0xc, 0x655e(%rip)        ; static SwiftTest.Car.num2 : Swift.Int + 4
    0x100000dda <+90>: addq   $0x30, %rsp
    0x100000dde <+94>: popq   %rbp
    0x100000ddf <+95>: retq 
           
Swift底層原理探索----屬性 &amp; 方法屬性方法下标

如上圖所示,首先我們可以快速定位

num1

num3

,我們可以先記錄一下他們的記憶體位址

  • &num1 = 0x6595 + 0x100000d93 = 0x100007328

  • &num3 = 0x655e + 0x100000dda = 0x100007338

num1

num2

中間,我們發現了一個叫

Car.num2.unsafeMutableAddressor

的函數被調用,并且通過将它的傳回值作為位址通路了一段記憶體空間,并向其指派

11

,從

Car.num2.unsafeMutableAddressor

這個名字,我們可以看出,這個函數傳回出來的位址,就是

Car.num2

的位址,首先我們運作到

0x100000dbd <+61>: movq $0xb, (%rax)

這句彙編,記錄一下這個位址的值

(lldb) register read rax
     rax = 0x0000000100007330  SwiftTest`static SwiftTest.Car.num2 : Swift.Int
           
👆👆👆可以看到,這個位址正好是

num1

num3

之間的那段空間,是以雖然

num2

作為

Car

static

存儲屬性,但是從它在記憶體中的位置來看,跟普通的全局變量沒有差別,是以可以說static存儲屬性的本質就是全局變量。

代碼稍微調整一下

var num1 = 10

class Car {
    static var num2 = 1
}
//Car.num2 = 11   //将這一句注釋掉
var num3 = 12


**********************👇對應彙編👇***********************
SwiftTest`main:
    0x100000dc0 <+0>:  pushq  %rbp
    0x100000dc1 <+1>:  movq   %rsp, %rbp
    0x100000dc4 <+4>:  xorl   %eax, %eax
->  0x100000dc6 <+6>:  movq   $0xa, 0x6557(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100000dd1 <+17>: movq   $0xc, 0x655c(%rip)        ; static SwiftTest.Car.num2 : Swift.Int + 4
    0x100000ddc <+28>: popq   %rbp
    0x100000ddd <+29>: retq
           
👆👆👆可以看出,彙編裡

Car.num2

相關的代碼就消失了,也就是說如果沒有用到

Car.num2

,那麼它是不會被初始化的,是以我們說

static

存儲屬性是預設

lazy

(延遲)的。

我們将代碼恢複,再次更深入的跟蹤一下彙編過程

var num1 = 10 // 斷點處
class Car {
    static var num2 = 1
}
Car.num2 = 11
var num3 = 12


**********************👇對應彙編👇***********************
SwiftTest`main:
    0x100000d80 <+0>:  pushq  %rbp
    0x100000d81 <+1>:  movq   %rsp, %rbp
    0x100000d84 <+4>:  subq   $0x30, %rsp
->  0x100000d88 <+8>:  movq   $0xa, 0x6595(%rip)        ; demangling cache variable for type metadata for Swift.Array<Swift.UInt8> + 4
    0x100000d93 <+19>: movl   %edi, -0x1c(%rbp)
    0x100000d96 <+22>: movq   %rsi, -0x28(%rbp)
    0x100000d9a <+26>: callq  0x100000e40               ; SwiftTest.Car.num2.unsafeMutableAddressor : Swift.Int at main.swift
    0x100000d9f <+31>: xorl   %ecx, %ecx
    0x100000da1 <+33>: movq   %rax, %rdx
    0x100000da4 <+36>: movq   %rdx, %rdi
    0x100000da7 <+39>: leaq   -0x18(%rbp), %rsi
    0x100000dab <+43>: movl   $0x21, %edx
    0x100000db0 <+48>: movq   %rax, -0x30(%rbp)
    0x100000db4 <+52>: callq  0x1000053a2               ; symbol stub for: swift_beginAccess
    0x100000db9 <+57>: movq   -0x30(%rbp), %rax
    0x100000dbd <+61>: movq   $0xb, (%rax)
    0x100000dc4 <+68>: leaq   -0x18(%rbp), %rdi
    0x100000dc8 <+72>: callq  0x1000053c6               ; symbol stub for: swift_endAccess
    0x100000dcd <+77>: xorl   %eax, %eax
    0x100000dcf <+79>: movq   $0xc, 0x655e(%rip)        ; static SwiftTest.Car.num2 : Swift.Int + 4
    0x100000dda <+90>: addq   $0x30, %rsp
    0x100000dde <+94>: popq   %rbp
    0x100000ddf <+95>: retq
           
Swift底層原理探索----屬性 &amp; 方法屬性方法下标

這一次我們從

unsafeMutableAddressor

這個函數跟進去看看

SwiftTest`Car.num2.unsafeMutableAddressor:
->  0x100000e40 <+0>:  pushq  %rbp
    0x100000e41 <+1>:  movq   %rsp, %rbp
    0x100000e44 <+4>:  cmpq   $-0x1, 0x64f4(%rip)       ; SwiftTest.num3 : Swift.Int + 7
    0x100000e4c <+12>: sete   %al
    0x100000e4f <+15>: testb  $0x1, %al
    0x100000e51 <+17>: jne    0x100000e55               ; <+21> at main.swift:719:16
    0x100000e53 <+19>: jmp    0x100000e5e               ; <+30> at main.swift
    0x100000e55 <+21>: leaq   0x64d4(%rip), %rax        ; static SwiftTest.Car.num2 : Swift.Int
    0x100000e5c <+28>: popq   %rbp
    0x100000e5d <+29>: retq   
    0x100000e5e <+30>: leaq   -0x45(%rip), %rax         ; globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0 at main.swift
    0x100000e65 <+37>: leaq   0x64d4(%rip), %rdi        ; globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_token0
    0x100000e6c <+44>: movq   %rax, %rsi
    0x100000e6f <+47>: callq  0x1000053fc               ; symbol stub for: swift_once
    0x100000e74 <+52>: jmp    0x100000e55               ; <+21> at main.swift:719:16
           

看到在最後,調用了

swift_once

函數,GCD裡面我們知道有個

dispatch_once

,是否有關聯呢,我們進入這個函數

libswiftCore.dylib`swift_once:
->  0x7fff73447820 <+0>:  pushq  %rbp
    0x7fff73447821 <+1>:  movq   %rsp, %rbp
    0x7fff73447824 <+4>:  cmpq   $-0x1, (%rdi)
    0x7fff73447828 <+8>:  jne    0x7fff7344782c            ; <+12>
    0x7fff7344782a <+10>: popq   %rbp
    0x7fff7344782b <+11>: retq   
    0x7fff7344782c <+12>: movq   %rsi, %rax
    0x7fff7344782f <+15>: movq   %rdx, %rsi
    0x7fff73447832 <+18>: movq   %rax, %rdx
    0x7fff73447835 <+21>: callq  0x7fff7349c19c            ; symbol stub for: dispatch_once_f
    0x7fff7344783a <+26>: popq   %rbp
    0x7fff7344783b <+27>: retq   
    0x7fff7344783c <+28>: nop    
    0x7fff7344783d <+29>: nop    
    0x7fff7344783e <+30>: nop    
    0x7fff7344783f <+31>: nop
           

真相出現了,原來

swift_once

函數裡面确實是調用了GCD的

dispatch_once_f

,那麼

dispatch_once

裡面的

block

是什麼呢,直覺告訴我們應該就是

Car.num2

的初始化代碼,也就是這句代碼

static var num2 = 1

如何證明呢?我先我們将彙編運作到

callq 0x7fff7349c19c ; symbol stub for: dispatch_once_f

處,因為此時,

dispatch_once_f

函數所需的參數按照彙編的慣例,已經放到了

rsi

rdx

等寄存起裡面了,我們可以檢視一下此時這兩個寄存器的内容

Swift底層原理探索----屬性 &amp; 方法屬性方法下标
(lldb) register read rsi
     rsi = 0x00007ffeefbff598
(lldb) register read rdx
     rdx = 0x0000000100000e20  SwiftTest`globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0 at main.swift
(lldb) 
           

可以看到

rdx

此時存放的是一個跟globalinit(全局初始化)相關的函數

func0

,位址為

0x0000000100000e20

,該函數就是

dispatch_once_f

所接受的

block

。接下來我們回到Swift源碼,在如下處加一個斷點

Swift底層原理探索----屬性 &amp; 方法屬性方法下标

那麼我們繼續運作程式,斷點會停在上面這句代碼上,如果我們猜測正确的話,那麼此時的彙編應該就在

globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0

這個函數裡面,我們運作程式後,彙編如下

SwiftTest`globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0:
    0x100000e20 <+0>:  pushq  %rbp
    0x100000e21 <+1>:  movq   %rsp, %rbp
->  0x100000e24 <+4>:  movq   $0x1, 0x6501(%rip)        ; SwiftTest.num1 : Swift.Int + 4
    0x100000e2f <+15>: popq   %rbp
    0x100000e30 <+16>: retq   
           

确實是處在

globalinit_33_B9B0E304FD1668A20F6C95C54E9E2F7A_func0

函數内部,并且這裡進行初始化的記憶體位址是

0x100000e2f + 0x6501 = 0x100007330

,從初始值很明顯看出這段記憶體就是

num2

,并且跟我們在

unsafeMutableAddressor

函數傳回處記錄的傳回值相同,結果正如預期,證明完畢。

👆👆👆在Swift底層,是通過

unsafeMutableAddressor

->

libswiftCore.dylib-swift_once

->

libswiftCore.dylib-dispatch_once_f:

---------->

static var num2 = 1

來對

num2

進行初始化的,因為使用了

GCD

dispatch_once

,是以我們說

static

存儲屬性是線程安全的,并且隻能被初始化一次。

方法

方法

class Car {
    static var count = 0  
    init() {
        Car.count += 1
    }
    // Type Method
    static func getCount() -> Int {
    	//以下幾種通路count的方法是等價的
    	count += 1
    	self.count += 1
    	Car.self.count += 1
    	Car.count += 1
     	return count 
     }
}

let c0 = Car()
let c1 = Car()
let c2 = Car()
print(Car.getCount()) // 通過類名進行調用
           

枚舉、結構體、類都可以定義執行個體方法、類型方法

  • 執行個體方法(

    Instance Method

    ):通過執行個體對象進行調用
  • 類型方法(

    Type Method

    ):通過類型調用,用

    static

    或者

    class

    關鍵字來定義

self

  • 在執行個體方法中就代表執行個體對象
  • 在類型方法中就代表類型

在類型方法

static func getCount

中,以下幾種寫法等價

  • count

  • self.count

  • Car.count

  • Car.self.count

mutating

Swift文法規定,對于結構體和枚舉這兩種值類型,預設情況下,他們的屬性是不能被自身的執行個體方法所修改的(對于類沒有這個規定)

  • func

    關鍵字前面加

    mutating

    就可以允許這種修改行為,如下
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(deltaX: Double, deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}

enum StateSwitch {
    case low, middle, high
    mutating func next() {
        switch self {
        case .low:
            self = .middle
        case .middle:
            self = .high
        case .high:
            self = .low
        }
    }
}
           

@discardableResult

在func前面加上@discardableResult,可以消除:函數調用後的傳回值未被使用的警告資訊⚠️

struct Point {
    var x = 0.0, y = 0.0
    @discardableResult mutating
    func moveX(deltaX: Double) -> Double {
        x += deltaX
        return x
    }
}
var p = Point()
p.moveX(deltaX: 10)
           

下标

使用

subscript

可以給任意類型(枚舉、類、結構體)增加下表功能。

subscript

的文法類似于執行個體方法、計算屬性,它的本質就是方法(函數)

class Point {
    var x = 0.0, y = 0.0
    subscript(index: Int) -> Double {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }

        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}

var p = Point()
p[0] = 11.1
p[1] = 22.2
print(p.x)  // 11.1
print(p.y)  // 22.2
print(p[0]) // 11.1
print(p[1]) // 22.2

           

從上面的案例來看,

subscript

為我們提供了通過

[i]

的方式去通路成員變量,就像數組/字典那樣去使用。下标與函數的表面差別,隻是在定義的時候,用

subscript

代替了

func funcName

,在調用的時候通過

[arg]

代替了

funcName(arg)

。而

subscript

的内部包含了

get

set

,很像計算屬性。

我們簡化一下代碼

class Point {
    var x = 0, y = 0
    subscript(index: Int) -> Int {
        set {
            if index == 0 {
                x = newValue
            } else if index == 1 {
                y = newValue
            }
        }

        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}

var p = Point()
p[0] = 10 // 0xa   在這裡放一個斷點⚠️
p[1] = 11 // 0xb
           

運作程式至斷點處,彙編如下

Swift底層原理探索----屬性 &amp; 方法屬性方法下标

我們我們根據立即數10和11,找到綠框處代碼,紅色标記處的函數顯然不是下标的調用,我們從兩個綠框處的間接函數調用跟進去看看

0x1000016b1 <+145>: callq  *0x98(%rcx) ---進入該函數-->

SwiftTest`Point.subscript.setter:
->  0x100001c10 <+0>:   pushq  %rbp
    0x100001c11 <+1>:   movq   %rsp, %rbp
    0x100001c14 <+4>:   pushq  %r13
    0x100001c16 <+6>:   subq   $0x48, %rsp
    0x100001c1a <+10>:  xorl   %eax, %eax
    0x100001c1c <+12>:  leaq   -0x10(%rbp), %rcx
    0x100001c20 <+16>:  movq   %rdi, -0x28(%rbp)
    ..........
    ..........
    ..........
           
0x100001715 <+245>: callq  *0x98(%rcx) ---進入該函數-->

SwiftTest`Point.subscript.setter:
->  0x100001c10 <+0>:   pushq  %rbp
    0x100001c11 <+1>:   movq   %rsp, %rbp
    0x100001c14 <+4>:   pushq  %r13
    0x100001c16 <+6>:   subq   $0x48, %rsp
    0x100001c1a <+10>:  xorl   %eax, %eax
    0x100001c1c <+12>:  leaq   -0x10(%rbp), %rcx
    0x100001c20 <+16>:  movq   %rdi, -0x28(%rbp)
     ..........
    ..........
    ..........
           

上面的結果說明

callq *0x98(%rcx)

=

Point.subscript.setter

等價于

p[i] =

是以,證明了下标的本質就是函數。

🍬🍬🍬這裡為什麼是

callq *[記憶體位址]

來間接調用函數呢,因為

p

不是一個函數名,而是一個變量,是以想要調用下标函數,是以肯定是通過

間接調用

的方式來操作的。

直接調用:

callq 函數位址

間接調用:

callq *記憶體位址

注意點⚠️

  • subscript

    中定義的傳回值類型可以決定:
    • get

      方法的傳回值類型
    • set

      方法中國呢

      newValue

      的類型
  • subscript

    可以接受多個參數,并且是任意類型

下标的細節

subscript

可以沒有

set

方法,但是必須要有

get

方法,如果隻有

get

方法,可以了解為隻讀

class Point {
    var x = 0.0, y = 0.0
    subscript(index: Int) -> Double {
        get {
            if index == 0 {
                return x
            } else if index == 1 {
                return y
            }
            return 0
        }
    }
}
           

如果隻有

get

方法,還可以省略

get

class Point {
    var x = 0.0, y = 0.0
    subscript(index: Int) -> Double {
        if index == 0 {
            return x
        } else if index == 1 {
            return y
        }
        return 0
    }
}
           

還可以設定參數标簽

class Point {
    var x = 0.0, y = 0.0
    subscript(index i: Int) -> Double {
        if i == 0 {
            return x
        } else if i == 1 {
            return y
        }
        return 0
    }
}

var p = Point()
p.y = 22.2
print(p[index: 1]) // 如果有标簽的話,在使用的時候,就一定要帶上标簽才行
           

上面我們看到的

subscript

都是相當于執行個體方法(預設),下标也可以是類型方法

class Sum {
    static subscript(v1: Int, v2: Int) -> Int {
        return v1 + v2
    }
}

print(Sum[10,20])

           

結構體、類作為傳回值的對比

struct Point {
    var x = 0
    var y = 0
}

class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        set { point = newValue }  // 如果後面有堆point進行指派,則必須要加上set方法。
        get { point }
    }
}

var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
print(pm[0])
print(pm.point)
           

上面的案例中,

PointManager

這個類有一個下标,傳回類型是結構體

struct Point

,并且注意這個下标的特點,無論下标值傳什麼,它傳回的都是結構體變量

point

,我們需要注意的是,下标裡面的

set

的寫法應該如下

這樣你可能會好奇,

pm[0].x = 11

或者

pm[0].y = 22

時,在set方法裡面我們怎麼知道這個

newValue

的值到底是給

.x

還是給

.y

的。其實你應該注意到,這裡的newValue應該是

struct Point

類型的,如果這樣,其實設計者的思路就不難猜到

pm[0].x = 11

—>

newValue = (11, pm[0].y)

—>

set { point = newValue = (11, pm[0].y) }

pm[0].y = 22

—>

newValue = (pm[0].x, 22)

—>

set { point = newValue = (pm[0].x, 22) }

如果把

strtct Point

換成

class Point

, 這個

set

方法就可以不用寫了

class Point {
    var x = 0
    var y = 0
}

class PointManager {
    var point = Point()
    subscript(index: Int) -> Point {
        get { point }
    }
}

var pm = PointManager()
pm[0].x = 11
pm[0].y = 22
print(pm[0])
print(pm.point)
           

因為我們通過

pm[0]

拿到的是

point

這個對象執行個體指針,那麼

pm[0].x

等價于

point.x

,是以

point.x = 11

是符合規範的。

下标接受多個參數

class Grid {
    var data = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8]
    
    ]
    
    subscript( row: Int, column: Int) -> Int {
        set {
            guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                return
            }
            data[row][column] = newValue
        }
        
        get  {
            guard row >= 0 && row < 3 && column >= 0 && column < 3 else {
                return 0
            }
            return data[row][column]
        }
    }
    
    
    
}

var grid = Grid()
grid[0, 1] = 77
grid[1, 2] = 88
grid[2, 0] = 99
print(grid.data)

*********************運作結果
[[0, 77, 2], [3, 4, 88], [99, 7, 8]]
Program ended with exit code: 0
           

好了,屬性和方法,暫時梳理到這裡,period!