专注、坚持

Swift 拾遗 - 属性

2020.09.18 by kingcos
Date Notes Info
2020-09-18 纳入 Swift 拾遗系列,并重新整理完善 Swift 5.3, Xcode 12.0
2017-04-27 扩充「延迟存储属性」部分并新增「devxoul/Then」一节 Swift 3.1, Xcode 8.3.2
2016-10-26 首次提交 Swift 3.0, Xcode 8.1 Beta 3

Preface

《Swift 拾遗》是一个关于 Swift 的新文章专辑,这个系列的文章将不会涉及基本语法的层面,而是尝试从底层「拾」起之前所忽视的内容。今天我们将一起简单探究 Swift 中的属性。

存储属性与计算属性

struct Foo {
    var bar: Int
    var baz: Int {
        get {
            bar * 2
        }

        set {
            bar = newValue / 2
        }
    }

    var customSetParamBaz: Int {
        get {
            bar * 2
        }

        set(customNewValue) {
            bar = customNewValue / 2
        }
    }
}

var foo = Foo(bar: 10)

_ = foo.bar // 10
_ = foo.baz // 20  // BREAKPOINT 🔴

foo.baz = 100

_ = foo.baz // 100
_ = foo.bar // 50

如上,bar 为存储属性,即将占用实例对象的内存空间;而 baz 为计算属性,将通过其它变量得出,当外界取值时将得到调用 get 中的返回,设置时将通过 setnewValue 将外界的值传递进入并进行一定的计算。那么计算属性是否会占用实例对象的内存空间呢?

print(MemoryLayout<Foo>.size)   // 8
print(MemoryLayout<Foo>.stride) // 8

我们也可以运行代码,并在断点处以汇编代码分析:

->  0x1000027a1 <+113>: movq   %rax, %rdi
    0x1000027a4 <+116>: leaq   -0x38(%rbp), %rsi
    0x1000027a8 <+120>: movl   $0x20, %edx
    0x1000027ad <+125>: callq  0x100003b92               ; symbol stub for: swift_beginAccess
    0x1000027b2 <+130>: movq   0x59bf(%rip), %rdi        ; demo.foo : demo.Foo
    ; 将调用计算属性的 getter
    0x1000027b9 <+137>: callq  0x1000028c0               ; demo.Foo.baz.getter : Swift.Int at main.swift:6
    0x1000027be <+142>: leaq   -0x38(%rbp), %rdi
    0x1000027c2 <+146>: movq   %rax, -0xa0(%rbp)
    0x1000027c9 <+153>: callq  0x100003b98               ; symbol stub for: swift_endAccess
    0x1000027ce <+158>: leaq   0x59a3(%rip), %rax        ; demo.foo : demo.Foo
    0x1000027d5 <+165>: xorl   %r8d, %r8d
    0x1000027d8 <+168>: movl   %r8d, %ecx
    0x1000027db <+171>: movq   %rax, %rdi
    0x1000027de <+174>: leaq   -0x50(%rbp), %rsi
    0x1000027e2 <+178>: movl   $0x21, %edx
    0x1000027e7 <+183>: callq  0x100003b92               ; symbol stub for: swift_beginAccess
    0x1000027ec <+188>: movl   $0x64, %edi
    0x1000027f1 <+193>: leaq   0x5980(%rip), %r13        ; demo.foo : demo.Foo
    ; 将调用计算属性的 setter
    0x1000027f8 <+200>: callq  0x100002960               ; demo.Foo.baz.setter : Swift.Int at main.swift:10
    0x1000027fd <+205>: leaq   -0x50(%rbp), %rdi
    0x100002801 <+209>: callq  0x100003b98               ; symbol stub for: swift_endAccess

如上,计算属性将不会产生实际的变量,外界的使用将通过 getset 中所生成的 gettersetter 方法访问或设置,因此也就不会占用实例对象的内存空间。

计算属性也支持只读(Read-only),只需要不实现 set 即可,此时也可省略显式的 get 关键字:

class Foo {
    var bar: Int = 0

    var readonlyBaz1: Int {
        get {
            bar * 2
        }
    }

    var readonlyBaz2: Int {
        bar * 2
    }
}

var foo = Foo()

// ERROR: Cannot assign to property: 'readonlyBaz1' is a get-only property
foo.readonlyBaz1 = 10
// ERROR: Cannot assign to property: 'readonlyBaz2' is a get-only property
foo.readonlyBaz2 = 10

属性(存储属性与计算属性)也支持为 getset 设置不同的访问控制级别:

class Foo {
    var bar: Int = 0

    internal private(set) var privateSetBaz3: Int {
        get {
            bar * 2
        }

        set {
            bar = newValue / 2
        }
    }

    private(set) var privateSetBaz4: Int {
        get {
            bar * 2
        }

        set {
            bar = newValue / 2
        }
    }

    func innerFunc() {
        self.privateSetBaz3 = 10
        self.privateSetBaz4 = 10
    }
}

var foo = Foo()

foo.innerFunc()

// ERROR: Cannot assign to property: 'privateSetBaz3' setter is inaccessible
foo.privateSetBaz3 = 10
// ERROR: Cannot assign to property: 'privateSetBaz4' setter is inaccessible
foo.privateSetBaz4 = 10

那么计算属性有何使用场景呢?其实当定义的属性依赖于其它属性时,就可以使用计算属性。枚举中的原始值 rawValue 就是通过只读计算属性实现的:

enum Foo: Int {
    case bar = 0, baz
}

_ = Foo.bar.rawValue // 0 // BREAKPOINT 🔴

尝试运行后在断点处查看汇编:

demo`main:
    0x100002500 <+0>:  pushq  %rbp
    0x100002501 <+1>:  movq   %rsp, %rbp
    0x100002504 <+4>:  subq   $0x20, %rsp
    0x100002508 <+8>:  xorl   %eax, %eax
    0x10000250a <+10>: movl   %edi, -0x4(%rbp)
->  0x10000250d <+13>: movl   %eax, %edi
    0x10000250f <+15>: movq   %rsi, -0x10(%rbp)
    ; 将调用计算属性的 getter
    0x100002513 <+19>: callq  0x1000025a0               ; demo.Foo.rawValue.getter : Swift.Int at <compiler-generated>
    0x100002518 <+24>: xorl   %ecx, %ecx
    0x10000251a <+26>: movq   %rax, -0x18(%rbp)
    0x10000251e <+30>: movl   %ecx, %eax
    0x100002520 <+32>: addq   $0x20, %rsp
    0x100002524 <+36>: popq   %rbp
    0x100002525 <+37>: retq

延迟存储属性

延迟存储属性是指该属性的初始化过程将延迟到首次使用时,而非跟随实例的创建而创建。当属性未被使用时,将避免消耗不必要的性能。延迟存储属性使用 lazy 关键字修饰:

struct Foo {
    lazy var bar: Int = {
        print("bar will init")

        return 1
    }()

    var baz: Int = {
        print("baz will init")

        return 1
    }()
}

_ = Foo()

// baz will init

如上,当构造 Foo 实例对象时,延迟存储属性将暂时不初始化。另外,延迟存储属性需要有几个注意点:首先,延迟存储属性必须使用 var 修饰,而不可使用 let 修饰。这是因为 let 常量一旦创建后就不能再改变,而延迟存储属性会在使用时再初始化将与此相悖;其次,结构体类型中若存在延迟存储属性,那么使用 let 声明的结构体常量将无法访问延迟存储属性,原因其实同理,在首次访问时将执行延迟存储属性的初始化,将会导致常量内存空间改变,也与常量本身的概念相悖;但 class 类型的变量由于本质是引用类型,即使堆空间的内存空间改变,也不会导致指针指向的内存空间地址改变,因此 class 类型的常量仍可以访问延迟存储属性:

class FooClass {
    lazy var a = 1
}

struct FooStruct {
    lazy var a = 1
}

let fc = FooClass()
print(fc.a)

let fs = FooStruct()
// ERROR: Cannot use mutating getter on immutable value: 'fs' is a 'let' constant
print(fs.a)

还需要注意的是,延迟存储属性是线程安全的,即有可能在多线程中被多次初始化:

import Foundation

struct Foo {
    lazy var bar: Int = {
        print("bar will init")

        return 1
    }()

     var baz: Int = {
        print("baz will init")

        return 1
    }()
}

var foo = Foo()

var array = [Int]()

DispatchQueue.concurrentPerform(iterations: 3) { _ in
    print("--- \(Thread.current) ---")
    print(foo.bar)
}

// baz will init
// --- <NSThread: 0x100611a40>{number = 3, name = (null)} ---
// --- <NSThread: 0x1029197f0>{number = 2, name = (null)} ---
// --- <NSThread: 0x10050c580>{number = 1, name = main} ---
// bar will init
// bar will init
// 1
// 1
// bar will init
// 1

如上,我们使用 GCD 的并发执行 API concurrentPerform 执行三次,如果延迟存储属性是线程安全的,那么将只会进入其构造方法一次,但此时其实输出了三句 bar will init

lazy 方法

let doubled = (0..<3).map { i -> Int in
    print("mapping")
    return i * 2
}

let lazyDoubled = (0..<3).lazy.map { i -> Int in
    print("lazy mapping")
    return i * 2
}

// mapping
// mapping
// mapping

lazyDoubled.forEach {
    print($0)
}

// lazy mapping
// 0
// lazy mapping
// 2
// lazy mapping
// 4

在 Swift 标准库中也有一些内置的 lazy 方法,它们的执行时机也将推迟到实际使用该方法返回值的时刻。

属性观察器

在 Swift 中,我们还可以对非延迟存储属性添加属性观察器,以便在值发生改变之前和之后进行一些操作:

struct Foo {
    var bar: Int {
        willSet {
            print("\(bar) will set to \(newValue)")
        }

        didSet {
            // oldValue 会在 willSet 进行保存
            print("\(oldValue) did set to \(bar)")
        }
    }

    var customValueParamsBar: Int = 0 {
        willSet(new) {
            print("\(customValueParamsBar) will set \(new)")
        }

        didSet(old) {
            print("\(old) did set to \(customValueParamsBar)")
        }
    }
}

var foo = Foo(bar: 10)
foo.bar = 20 // BREAKPOINT 🔴

// 10 will set to 20
// 10 did set to 20

如上,在值被真正改变之前,将先通过 willSet 方法,新值将默认可以通过 newValue 变量访问,此时因为尚未执行赋值,因此旧值即可通过变量本身 bar 访问;设置后同理可通过 didSet 方法,此时旧值将默认可以通过 oldValue 变量访问,新值则可直接通过变量本身 bar 访问。

尝试运行后在断点处查看汇编:

->  0x100001c2e <+62>:  movq   %rcx, %rdi
    0x100001c31 <+65>:  leaq   -0x20(%rbp), %rax
    0x100001c35 <+69>:  movq   %rsi, -0x38(%rbp)
    0x100001c39 <+73>:  movq   %rax, %rsi
    0x100001c3c <+76>:  movl   $0x21, %edx
    0x100001c41 <+81>:  movq   -0x38(%rbp), %rcx
    0x100001c45 <+85>:  callq  0x100003bc0               ; symbol stub for: swift_beginAccess
    0x100001c4a <+90>:  movl   $0x14, %edi
    0x100001c4f <+95>:  leaq   0x655a(%rip), %r13        ; demo.foo : demo.Foo
    ; 将调用 setter
    0x100001c56 <+102>: callq  0x100001cb0               ; demo.Foo.bar.setter : Swift.Int at main.swift:4
    0x100001c5b <+107>: leaq   -0x20(%rbp), %rdi
    0x100001c5f <+111>: callq  0x100003bd2               ; symbol stub for: swift_endAccess

demo`Foo.bar.setter:
->  0x100001cb0 <+0>:  pushq  %rbp
    0x100001cb1 <+1>:  movq   %rsp, %rbp
    0x100001cb4 <+4>:  pushq  %r13
    0x100001cb6 <+6>:  subq   $0x38, %rsp
    0x100001cba <+10>: movq   $0x0, -0x10(%rbp)
    0x100001cc2 <+18>: movq   $0x0, -0x18(%rbp)
    0x100001cca <+26>: movq   $0x0, -0x20(%rbp)
    0x100001cd2 <+34>: movq   %rdi, -0x10(%rbp)
    0x100001cd6 <+38>: movq   %r13, -0x18(%rbp)
    0x100001cda <+42>: movq   (%r13), %rax
    0x100001cde <+46>: movq   %rax, -0x20(%rbp)
    0x100001ce2 <+50>: movq   %rdi, -0x28(%rbp)
    0x100001ce6 <+54>: movq   %r13, -0x30(%rbp)
    0x100001cea <+58>: movq   %rax, -0x38(%rbp)
    ; 将调用 willset
    0x100001cee <+62>: callq  0x100001d20               ; demo.Foo.bar.willset : Swift.Int at main.swift:5
    0x100001cf3 <+67>: movq   -0x30(%rbp), %rax
    0x100001cf7 <+71>: movq   -0x28(%rbp), %rcx
    0x100001cfb <+75>: movq   %rcx, (%rax)
    0x100001cfe <+78>: movq   -0x38(%rbp), %rdi
    0x100001d02 <+82>: movq   %rax, %r13
    ; 将调用 didset
    0x100001d05 <+85>: callq  0x100001fd0               ; demo.Foo.bar.didset : Swift.Int at main.swift:9
    0x100001d0a <+90>: addq   $0x38, %rsp
    0x100001d0e <+94>: popq   %r13
    0x100001d10 <+96>: popq   %rbp
    0x100001d11 <+97>: retq

此时可以发现,即使我们改变的变量本质是存储属性,但编译器仍然为其生成了 setter 方法,并在该行执行 si 进入后可以发现,即 setter 方法中先调用了 willSet,并在随后调用了 didSet

另外需要注意的是,初始化方法或在属性定义时给予的默认值时,将不会触发属性观察器;另外,在 willSet 中如果继续编写更改原有变量值的代码时,既不会再次触发属性观察器,但更改也不会生效,但在 didSet 中,虽然也不会再次触发属性观察器,但更改将会生效:

struct Foo {
    var bar: Int {
        willSet {
            // WARNING: Attempting to store to property 'bar' within its own willSet, which is about to be overwritten by the new value
            bar = 1000
            print("\(bar) will set to \(newValue)")
        }

        didSet {
            bar = 2000
            print("\(oldValue) did set to \(bar)")
            bar = 3000
        }
    }
}

var foo = Foo(bar: 10)
foo.bar = 20

print(foo.bar)

// 10 will set to 20
// 10 did set to 2000
// 3000

如上,虽然我们可以了解这一行为,但在属性观察器中再次进行赋值的操作本身就有些诡异,因此应当避免。

计算属性、属性观察器与 inout

在上面的示例中,我们通常直接通过实例对象对变量本身进行了更改,并触发了属性观察器,那么如果我们将它们传递给含有 inout 参数的方法并进行更改时,还会调用相应的 willSetdidSet 方法吗?

首先我们创建一个名为 Foo 的结构体类型,其中包含普通的存储属性、带有属性观察器的属性、和计算属性,并创建名为 inoutFunc(_:) 带有 inout 修饰参数的方法:

struct Foo: CustomStringConvertible {
    var a = 1

    var b = 1 {
        willSet {
            print("b(\(b)) will set to \(newValue)")
        }

        didSet {
            print("b(\(oldValue)) did set to \(b)")
        }
    }

    var c: Int {
        get {
            print("c get")
            return a * 10
        }

        set {
            print("c set")
            a = newValue / 10
        }
    }

    var description: String {
        "a: \(a), b: \(b), c: \(c)"
    }
}

func inoutFunc(_ param: inout Int) {
    param = 100
}

var foo = Foo()

print(foo)

// c get
// a: 1, b: 1, c: 10

首先我们尝试将不带属性观察器的变量传递到 inoutFunc 中:

inoutFunc(&foo.a) // BREAKPOINT 🔴

print(foo)

// c get
// a: 100, b: 1, c: 1000

尝试运行后在断点处查看汇编:

->  0x100001958 <+296>: movq   %rax, %rdi
    0x10000195b <+299>: leaq   -0x30(%rbp), %rsi
    0x10000195f <+303>: movl   $0x21, %edx
    0x100001964 <+308>: callq  0x100003a3c               ; symbol stub for: swift_beginAccess
    ; 将 rip 寄存器中的地址值加上 0x6850(根据注释,该地址对应的内容为 demo.foo)并存储到 rdi 寄存器(函数第一个参数)中
    0x100001969 <+313>: leaq   0x6850(%rip), %rdi        ; demo.foo : demo.Foo
    0x100001970 <+320>: callq  0x1000026f0               ; demo.inoutFunc(inout Swift.Int) -> () at main.swift:33

由于结构体变量的首地址就等同于其中第一个声明的变量的地址,因此此时存储属性即按地址传递到 inout 修饰参数的方法中。那么带有属性观察器的存储属性呢?

// inoutFunc(&foo.a)

inoutFunc(&foo.b) // BREAKPOINT 🔴

print(foo)

// b(1) will set to 100
// b(1) did set to 100
// c get
// a: 1, b: 100, c: 10

通过输出结果,我们可以看出属性观察器同样也被执行了。尝试运行后在断点处查看汇编:

->  0x100001942 <+50>:  movq   %rcx, %rdi
    0x100001945 <+53>:  leaq   -0x20(%rbp), %rax
    0x100001949 <+57>:  movq   %rsi, -0x58(%rbp)
    0x10000194d <+61>:  movq   %rax, %rsi
    0x100001950 <+64>:  movl   $0x21, %edx
    0x100001955 <+69>:  movq   -0x58(%rbp), %rcx
    0x100001959 <+73>:  callq  0x100003a2c               ; symbol stub for: swift_beginAccess
    0x10000195e <+78>:  movq   0x6863(%rip), %rax        ; demo.foo : demo.Foo + 8
    0x100001965 <+85>:  movq   %rax, -0x28(%rbp)
    0x100001969 <+89>:  leaq   -0x28(%rbp), %rdi
    0x10000196d <+93>:  callq  0x1000026e0               ; demo.inoutFunc(inout Swift.Int) -> () at main.swift:33
    0x100001972 <+98>:  movq   -0x28(%rbp), %rdi
    0x100001976 <+102>: leaq   0x6843(%rip), %r13        ; demo.foo : demo.Foo
    ; 将调用 setter
    0x10000197d <+109>: callq  0x100001b30               ; demo.Foo.b.setter : Swift.Int at main.swift:6

与之前不同的是,当执行完 inoutFunc 后,setter 仍然得到了调用,但具体原因我们先继续看下计算属性。我们知道计算属性并不需要内存来存储结果,而其结果来自于其它属性的计算。因此即使 inoutFunc 也并不会获取到变量 c 的内存地址:

// inoutFunc(&foo.a)

// inoutFunc(&foo.b)

inoutFunc(&foo.c) // BREAKPOINT 🔴

print(foo)

// c get
// c set
// c get
// a: 10, b: 1, c: 100

尝试运行后在断点处查看汇编:

->  0x100001932 <+50>:  movq   %rcx, %rdi
    0x100001935 <+53>:  leaq   -0x20(%rbp), %rax
    0x100001939 <+57>:  movq   %rsi, -0x58(%rbp)
    0x10000193d <+61>:  movq   %rax, %rsi
    0x100001940 <+64>:  movl   $0x21, %edx
    0x100001945 <+69>:  movq   -0x58(%rbp), %rcx
    0x100001949 <+73>:  callq  0x100003a2c               ; symbol stub for: swift_beginAccess
    0x10000194e <+78>:  movq   0x686b(%rip), %rdi        ; demo.foo : demo.Foo
    0x100001955 <+85>:  movq   0x686c(%rip), %rsi        ; demo.foo : demo.Foo + 8
    ; 将调用计算属性的 getter
    0x10000195c <+92>:  callq  0x1000020d0               ; demo.Foo.c.getter : Swift.Int at main.swift:17
    0x100001961 <+97>:  movq   %rax, -0x28(%rbp)
    0x100001965 <+101>: leaq   -0x28(%rbp), %rdi
    ; 将调用 inoutFunc
    0x100001969 <+105>: callq  0x1000026e0               ; demo.inoutFunc(inout Swift.Int) -> () at main.swift:33
    0x10000196e <+110>: movq   -0x28(%rbp), %rdi
    0x100001972 <+114>: leaq   0x6847(%rip), %r13        ; demo.foo : demo.Foo
    ; 将调用计算属性的 setter
    0x100001979 <+121>: callq  0x100002220               ; demo.Foo.c.setter : Swift.Int at main.swift:22

根据汇编,当遇到计算属性或带有存储属性观察器的存储属性时,将先通过 getter 将值拷贝到一个外界的局部变量上,再将该变量的地址传递到 inoutFunc 方法中,得到结果后外界变量将重新通过 setter 将值设置回原有的计算属性。因此属性观察器也是同理被触发的,而由于属性观察器只包含对设置值的不同时机的监听,因此看不到 getter 的过程。

类型属性

类型属性指的是通过类型直接访问的属性。我们在结构体、类、枚举中均可以定义类型属性,通常使用 static 关键字;在类中,如果定义的类型属性是计算属性,那么也可以使用 class 关键字:

struct Foo {
    static var foo: Int = 0
}

class Bar {
    static var bar1: Int = 0
    static let bar2: Int = 0

    class var bar3: Int {
        0
    }
}

enum Baz {
    static var baz = 0
}

print(Foo.foo)  // 0
print(Bar.bar1) // 0
print(Bar.bar2) // 0
print(Bar.bar3) // 0
print(Baz.baz)  // 0

类型属性其实类似于全局变量,即只会在内存中保存一份,且实际在内存中与全局变量存储在同一位置:

var foo = 10

class Bar {
    static var bar1: Int = 20
}

Bar.bar1 = 21 // BREAKPOINT 🔴
Bar.bar1 = 22

var baz = 30

尝试运行后在断点处查看汇编:

demo`main:
    0x100002940 <+0>:   pushq  %rbp
    0x100002941 <+1>:   movq   %rsp, %rbp
    0x100002944 <+4>:   subq   $0x50, %rsp
    ; 全局变量 foo:移动立即数 10 到 rip + 0x5955(0x1000082A8)
    0x100002948 <+8>:   movq   $0xa, 0x5955(%rip)        ; Swift51Overrides + 180
    0x100002953 <+19>:  movl   %edi, -0x34(%rbp)
    0x100002956 <+22>:  movq   %rsi, -0x40(%rbp)
    0x10000295a <+26>:  callq  0x100002a20               ; demo.Bar.bar.unsafeMutableAddressor : Swift.Int at main.swift
    0x10000295f <+31>:  xorl   %ecx, %ecx
    0x100002961 <+33>:  movq   %rax, %rdx
    0x100002964 <+36>:  movq   %rdx, %rdi
    0x100002967 <+39>:  leaq   -0x18(%rbp), %rsi
    0x10000296b <+43>:  movl   $0x21, %edx
    0x100002970 <+48>:  movq   %rax, -0x48(%rbp)
    0x100002974 <+52>:  callq  0x100003cd2               ; symbol stub for: swift_beginAccess
    0x100002979 <+57>:  movq   -0x48(%rbp), %rax
    ; (lldb) register read rax => rax = 0x00000001000082b0  demo`static demo.Bar.bar : Swift.Int
    ; 类型属性 Bar.bar:移动立即数 21 到 rax 寄存器中地址对应的内存空间
->  0x10000297d <+61>:  movq   $0x15, (%rax)
    0x100002984 <+68>:  leaq   -0x18(%rbp), %rdi
    0x100002988 <+72>:  callq  0x100003cde               ; symbol stub for: swift_endAccess
    0x10000298d <+77>:  callq  0x100002a20               ; demo.Bar.bar.unsafeMutableAddressor : Swift.Int at main.swift
    0x100002992 <+82>:  xorl   %r8d, %r8d
    0x100002995 <+85>:  movl   %r8d, %ecx
    0x100002998 <+88>:  movq   %rax, %rdx
    0x10000299b <+91>:  movq   %rdx, %rdi
    0x10000299e <+94>:  leaq   -0x30(%rbp), %rsi
    0x1000029a2 <+98>:  movl   $0x21, %edx
    0x1000029a7 <+103>: movq   %rax, -0x50(%rbp)
    0x1000029ab <+107>: callq  0x100003cd2               ; symbol stub for: swift_beginAccess
    0x1000029b0 <+112>: movq   -0x50(%rbp), %rax
    ; (lldb) register read rax => rax = 0x00000001000082b0  demo`static demo.Bar.bar : Swift.Int
    ; 类型属性 Bar.bar:移动立即数 22 到 rax 寄存器中地址对应的内存空间
    0x1000029b4 <+116>: movq   $0x16, (%rax)
    0x1000029bb <+123>: leaq   -0x30(%rbp), %rdi
    0x1000029bf <+127>: callq  0x100003cde               ; symbol stub for: swift_endAccess
    0x1000029c4 <+132>: xorl   %eax, %eax
    ; 全局变量 baz:移动立即数 30 到 rip + 0x58e7(0x1000082B8)
    0x1000029c6 <+134>: movq   $0x1e, 0x58e7(%rip)       ; static demo.Bar.bar : Swift.Int + 4
    0x1000029d1 <+145>: addq   $0x50, %rsp
    0x1000029d5 <+149>: popq   %rbp
    0x1000029d6 <+150>: retq

; 全局变量 foo:0x1000082A8
; 类型属性 Bar.bar:0x00000001000082b0(foo:0x1000082B0)
; 全局变量 baz:0x1000082B8

如上,三个变量均是在内存中占用了连续的存储空间,也证明了类型属性本质也是存储在全局区,其在整个程序生命周期中只会有一份。只是类型属性定义类的内部,需要通过类名来访问,以及可以额外使用一些访问控制。

需要注意的是,由于类型属性非针对于某一实例对象,因此其必须在声明时即给定初始值;类型属性如果同时为存储属性,默认即为 lazy 延迟:

struct Foo {
    static var foo: Int = 0

    static var calculateFoo: Int = {
        print("calculateFoo init")
        return 0
    }()
}

// 0
// calculateFoo init

但此时的延迟存储属性是线程安全的:

import Foundation

struct Foo {
    static var foo: Int = 0

    static var calculateFoo: Int = {
        print("calculateFoo init")
        return 0
    }()
}

print(Foo.foo)

DispatchQueue.concurrentPerform(iterations: 5) { _ in
    print("--- \(Thread.current) ---")
    print(Foo.calculateFoo)
}

// 0
// --- <NSThread: 0x1020be860>{number = 4, name = (null)} ---
// calculateFoo init
// 0
// --- <NSThread: 0x1007042d0>{number = 3, name = (null)} ---
// 0
// --- <NSThread: 0x1021040c0>{number = 2, name = (null)} ---
// 0
// --- <NSThread: 0x10200ab30>{number = 1, name = main} ---
// 0
// --- <NSThread: 0x1022040c0>{number = 5, name = (null)} ---
// 0
// 0

如上,即使同时 5 个线程并发访问,calculateFoo 也将被保证只初始化一次。那么如何证明其是线程安全的呢?

class Bar {
    static var bar: Int = 20 // BREAKPOINT 🔴
}

Bar.bar = 21 // BREAKPOINT 🔴

尝试运行后在断点处查看汇编:

demo`main:
    0x100002990 <+0>:  pushq  %rbp
    0x100002991 <+1>:  movq   %rsp, %rbp
    0x100002994 <+4>:  subq   $0x30, %rsp
    0x100002998 <+8>:  movl   %edi, -0x1c(%rbp)
    0x10000299b <+11>: movq   %rsi, -0x28(%rbp)
->  0x10000299f <+15>: callq  0x100002a20               ; demo.Bar.bar.unsafeMutableAddressor : Swift.Int at main.swift
    ; 调用后返回值(即 Bar.bar 的内存地址)存储在 rax 寄存器
    0x1000029a4 <+20>: xorl   %ecx, %ecx
    0x1000029a6 <+22>: movq   %rax, %rdx
    0x1000029a9 <+25>: movq   %rdx, %rdi
    0x1000029ac <+28>: leaq   -0x18(%rbp), %rsi
    0x1000029b0 <+32>: movl   $0x21, %edx
    0x1000029b5 <+37>: movq   %rax, -0x30(%rbp)
    0x1000029b9 <+41>: callq  0x100003cd2               ; symbol stub for: swift_beginAccess
    0x1000029be <+46>: movq   -0x30(%rbp), %rax
    ; (lldb) register read rax => rax = 0x0000000100008378  demo`static demo.Bar.bar : Swift.Int
    ; 移动立即数 21 到 rax(0x0000000100008378,即 Bar.bar 的内存地址)
    0x1000029c2 <+50>: movq   $0x15, (%rax)
    0x1000029c9 <+57>: leaq   -0x18(%rbp), %rdi
    0x1000029cd <+61>: callq  0x100003cde               ; symbol stub for: swift_endAccess
    0x1000029d2 <+66>: xorl   %eax, %eax
    0x1000029d4 <+68>: addq   $0x30, %rsp
    0x1000029d8 <+72>: popq   %rbp
    0x1000029d9 <+73>: retq

demo`Bar.bar.unsafeMutableAddressor:
    0x100002a20 <+0>:  pushq  %rbp
    0x100002a21 <+1>:  movq   %rsp, %rbp
->  0x100002a24 <+4>:  cmpq   $-0x1, 0x587c(%rip)       ; Swift51Overrides + 183
    0x100002a2c <+12>: sete   %al
    0x100002a2f <+15>: testb  $0x1, %al
    0x100002a31 <+17>: jne    0x100002a35               ; <+21> at main.swift:4:16
    0x100002a33 <+19>: jmp    0x100002a3e               ; <+30> at main.swift
    ; 将 rip + 0x593c(0x100008378,即 Bar.bar 的内存地址)放在 rax 寄存器中(即返回值)
    0x100002a35 <+21>: leaq   0x593c(%rip), %rax        ; static demo.Bar.bar : Swift.Int
    0x100002a3c <+28>: popq   %rbp
    ; 函数返回
    0x100002a3d <+29>: retq
    ; 将类型属性初始化代码所在闭包的函数地址存储在 rax 寄存器
    0x100002a3e <+30>: leaq   -0x45(%rip), %rax         ; globalinit_33_8E34543FA00B3A97BCFBD7BFA4EE5AD5_func0 at main.swift
    0x100002a45 <+37>: leaq   0x585c(%rip), %rdi        ; globalinit_33_8E34543FA00B3A97BCFBD7BFA4EE5AD5_token0
    ; 移动 rax 寄存器中的内容到 rsi 寄存器(作为 swift_once 函数第一个参数)
    0x100002a4c <+44>: movq   %rax, %rsi
    ; 从 swift_once 开始 si,直到进入 libswiftCore.dylib`swift_once:
    0x100002a4f <+47>: callq  0x100003ce4               ; symbol stub for: swift_once
    ; 跳转到 0x100002a35 ⬆️
    0x100002a54 <+52>: jmp    0x100002a35               ; <+21> at main.swift:4:16

libswiftCore.dylib`swift_once:
->  0x7fff6806a820 <+0>:  pushq  %rbp
    0x7fff6806a821 <+1>:  movq   %rsp, %rbp
    0x7fff6806a824 <+4>:  cmpq   $-0x1, (%rdi)
    0x7fff6806a828 <+8>:  jne    0x7fff6806a82c            ; <+12>
    0x7fff6806a82a <+10>: popq   %rbp
    0x7fff6806a82b <+11>: retq
    0x7fff6806a82c <+12>: movq   %rsi, %rax
    0x7fff6806a82f <+15>: movq   %rdx, %rsi
    0x7fff6806a832 <+18>: movq   %rax, %rdx
    ; swift_once 内部实际调用了 dispatch_once_f
    0x7fff6806a835 <+21>: callq  0x7fff680bf19c            ; symbol stub for: dispatch_once_f
    0x7fff6806a83a <+26>: popq   %rbp
    0x7fff6806a83b <+27>: retq
    0x7fff6806a83c <+28>: nop
    0x7fff6806a83d <+29>: nop
    0x7fff6806a83e <+30>: nop
    0x7fff6806a83f <+31>: nop

demo`globalinit_33_8E34543FA00B3A97BCFBD7BFA4EE5AD5_func0:
    0x100002a00 <+0>:  pushq  %rbp
    0x100002a01 <+1>:  movq   %rsp, %rbp
    ; 移动立即数 20 到 rip + 0x5969(0x100008378,即 Bar.bar 的内存地址)
->  0x100002a04 <+4>:  movq   $0x14, 0x5969(%rip)       ; getExistentialTypeMetadata(swift::ProtocolClassConstraint, swift::TargetMetadata<swift::InProcess> const*, unsigned long, swift::TargetProtocolDescriptorRef<swift::InProcess> const*)::$_3::operator()() const::TheLazy + 12
    0x100002a0f <+15>: popq   %rbp
    0x100002a10 <+16>: retq

因此类型属性默认即是 lazy 延迟初始化的,并由 swift_once 内的 dispatch_once_f 保证了线程安全。

类型属性通常可以用于单例模式,即整个生命周期内仅初始化一次:

class SomeManager {
    static var manager: SomeManager = {
        var manager = SomeManager()
        manager.foo = 10
        return manager
    }()

    var foo: Int = 0

    private init() {}

    func bar() {
        print(#function, foo)
    }
}

SomeManager.manager.bar()

// bar() 10

Reference