天天看点

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!