屬性
struct Circle {
//存儲屬性
var radius: Double
//計算屬性
var diamiter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
-
Swift中跟實例相關的屬性可以分為2大類
- 存儲屬性(
Stored Property
)- 類似于成員變量這個概念
-
存儲在實例的內存中 image
-
結構體、類可以定義存儲屬性 image
- 枚舉
不可以
定義存儲屬性image
我們知道枚舉的內存里面可以存放的是所有的case
以及關聯值
,并沒有所謂的成員變量概念,可因此也不存在所謂的存儲屬性
- 計算屬性(
Computed Property
)- 本質就是方法(函數)這個也可以通過匯編來證明一下 imageimageimageimage
-
不占用實例的內存 image
- 枚舉、結構體、類都可以定義計算屬性
- 本質就是方法(函數)這個也可以通過匯編來證明一下
- 存儲屬性(
存儲屬性
- 關于存儲屬性,
Swift
有個明確的規定- 在創建類 或 結構體的時候,必須為所有的存儲屬性設置一個合適的初始值,也就是要求類/結構體創建實例后,它的全部內存要得到初始化,而存儲屬性正好就是放在實例的內存里面的,所以需要將所有的存儲屬性設置初始值。
-
可以在初始化器里為存儲屬性設置一個初始值 imageimage
-
可以分配一個默認的屬性值作為屬性定義的一部分
-
可以在初始化器里為存儲屬性設置一個初始值
- 在創建類 或 結構體的時候,必須為所有的存儲屬性設置一個合適的初始值,也就是要求類/結構體創建實例后,它的全部內存要得到初始化,而存儲屬性正好就是放在實例的內存里面的,所以需要將所有的存儲屬性設置初始值。
計算屬性
-
set
傳入的新值默認叫做newValue
,也可以自定義 - 定義計算屬性只能用
var
, 不能用let
-
let
代表常量,也就是值是一成不變的 - 計算屬性的值是可能發生變化的(即使是只讀計算屬性)
-
-
只讀計算屬性:只有
get
, 沒有set
枚舉rawValue原理
- 枚舉原始值
rawValue
的本質是:只讀計算屬性,直接看匯編就可以證明imageimageimage
延遲存儲屬性(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
才能訪問延遲存儲屬性
因為延遲屬性初始化時需要改變結構體的內存
image
案例中,因為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
調用之前,參數的傳遞情況如下
對于上述比較簡單的情況,我們知道
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
參數傳遞流程如下圖
所以對于普通的存儲屬性
,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
這一次從匯編代碼量就可以判斷,對于計算屬性的處理肯定比存儲屬性要復雜,還是通過圖例來展示一下整個過程
可以看出,由于計算屬性在實例內部沒有對應的內存空間,編譯器通過在函數棧里面開辟一個局部變量的方法,利用它作為計算屬性的值的臨時宿主,并且將該局部變量的地址作為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
這次,我們發現跟計算屬性有些類似,這里也用到了函數棧的局部變量,它的作用是用來承載計算屬性的值,然后被傳入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
原來,這個
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
初始化器來初始化存儲屬性image -
存儲類型屬性默認就是
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
我們來算一下他們的實際內存地址
&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
如上圖所示,首先我們可以快速定位
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
這一次我們從
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
等寄存起里面了,我們可以查看一下此時這兩個寄存器的內容
(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源碼,在如下處加一個斷點
那么我們繼續運行程序,斷點會停在上面這句代碼上,如果我們猜測正確的話,那么此時的匯編應該就在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
運行程序至斷點處,匯編如下
我們我們根據立即數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
的寫法應該如下
set { point = newValue }
這樣你可能會好奇,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!