从零学习Swift 07:属性

Swift 中的属性分为两大类:存储属性 , 计算属性
一: 存储属性
存储属性类似于成员变量,定义方式很简单:
//存储属性
class Person{
var name: String = "张三"
}
存储属性存储在成员变量中;结构体Struct
和类Class
都可以定义存储属性,唯独枚举不可以.因为枚举变量的内存中只用于存储
case 值和关联值,没有用来存储存储属性的内存.
另外需要注意的是,在初始化类和结构体的实例时,必须为所有的存储属性设置初始值.
二: 计算属性
计算属性的定义方式需要用到set , get
关键字:
//计算属性
var age: Int{
set{
...
}
get{
...
}
}
像上面的age
就是一个计算属性.为了更生动的理解计算属性,我们以游戏账号升级为例子.假如规则是这样:在线两个小时,游戏账号升级一级:
//计算属性
struct Game{
//游戏时长,单位 : 小时
var time: Int
//游戏等级,在线两个小时升一级
var grade: Int {
set{
time = newValue * 2
}
get{
time / 2
}
}
}
//初始化 游戏时长为 2 个小时
var game1 = Game(time: 2)
print("在线 \(game1.time) 个小时, 游戏等级为 \(game1.grade) 级")
//设置游戏等级为 30 级
game1.grade = 30
print("在线 \(game1.time) 个小时, 游戏等级为 \(game1.grade) 级")
计算属性的本质就是方法,这一点我们可以通过汇编直接看出来:

既然计算属性的本质是方法,就说明计算属性是不会占用实例变量的内存.因为枚举,结构体,类中都可以定义方法,所以同样也可以定义计算属性.需要注意的是,计算属性只能用
var,不能用
let.
因为计算属性是会变化的.
枚举rawValue
的本质
我们之前在从零学习Swift 02:枚举和可选项 中说过,枚举是不会存储原始值的,今天我们就来搞清楚枚举rawValue
的本质.
首先看一下系统默认的rawValue
:

我们可以写个函数,实现rawValue
的功能:

但是很奇怪,为什么系统的rawValue
没有括号()
,其实它是计算属性

现在我们就搞清楚了
rawValue的本质其实就是只读的,计算属性
另外我们也发现,计算属性可以只有get
没有set
,那可不可以只有set
,没有get
呢?不可以,编译器会直接报错.

三: 延迟存储属性
使用lazy
可以定义一个延迟存储属性,在第一次使用属性的时候才会初始化.类似于 OC 中的懒加载.

如上图只创建了person
,还没有使用car
,car
就已经初始化了.
我们在car
前面加上lazy
关键字:

- 使用
lazy
延迟存储属性时要注意一下几点:
-
lazy
属性必须是var
,不能是let
.因为Swift
规定let
必须在实例初始化方法完成之前就有值.而lazy
是用到的时候才初始化,这就冲突了. -
lazy
属性不是线程安全的,多个线程同时访问同一个lazy
属性,可能不止加载一次. - 当结构体包含一个延迟存储属性时,只有
var
修饰的实例才能访问延迟存储属性,let
修饰的实例不允许访问延迟存储属性,什么意思呢?看下面一张图就知道了:
结构体 let 实例
四: 属性观察器
属性观察器类似于 OC 中的 KVO
,它的定义方式如下:

使用属性观察器必须满足三个条件:
- 必须是非
lazy
修饰(因为 lazy 属性是在第一次访问属性的时候才创建的,而添加属性观察器可能会打破 lazy 的机制
) - 必须是
var
变量(既然是属性观察器,肯定是观察属性值的变化,如果用 let 常量就没有任何意义了
) - 必须是存储属性(
因为计算属性内部本来就有一个
set,可以把监听代码写到
set中.
)
思考一下为什么计算属性不能设置属性观察器?
因为计算属性内部本来就有一个set
,可以把监听代码写到set
中.
五: inout 参数
之前在从零学习Swift 01:了解基础语法中用汇编分析过inout
参数,知道inout
输入输出参数是引用传递.今天使用更复杂的类型更深入的研究inout
参数的本质.
示例代码:
//矩形
struct Rectangle{
//存储属性 长
var length: Int
//属性观察器 宽
var width: Int{
willSet{
print("newValue : ",newValue)
}
didSet{
print("newValue : \(width) , oldValue : \(oldValue)")
}
}
//计算属性 (计算属性不占用实例内存空间,本质是方法)
//面积
var area: Int{
set{
length = newValue / width
}
get{
return length * width
}
}
func show(){
print("长方形的长 length = \(length) , 宽 width = \(width) , 面积 area = \(area)")
}
}
var rect = Rectangle(length: 10, width: 4)
rect.show()
func test(_ num: inout Int){
num = 20
}
test(&rect.length)
rect.show()
上面代码结构体中分别有存储属性,属性观察器,计算属性.下面我们就分别把这三种属性传入inout
参数.
-
inout
参数之存储属性
分析汇编:

从上图可以看到,调用test()
时直接把全局变量rect
的地址作为参数传入进去.为什么不是把length
的地址传进去呢?因为length
是结构体的第一个成员,所以结构体的地址就是length
的地址.这里传入rect
的地址和length
地址是等价的.
-
inout
参数之计算属性

分析汇编:

从汇编语言中可以看到,当inout
参数传入的是计算属性时,在调用test()
方法之前会先调用计算属性的getter
方法取出值,并且把值存入栈空间;然后再调用test()
方法,并且把栈空间的地址作为参数传递进去.所以在test()
方法内部修改的是栈空间的值;最后再调用计算属性的setter
方法,从栈空间中取出值传入setter
方法,并赋值.
图解:

3.inout
参数之属性观察器

分析汇编:

从上图的汇编中可以看到,inout
参数是属性观察器时,内部逻辑和计算属性很相似,都是取出值放到栈空间,然后修改栈空间的值.
为什么属性观察器不能像存储属性那样,直接传入地址,直接修改呢?因为属性观察器涉及到监听的逻辑.我们看看第三步的setter
方法的汇编:

可以看到setter
方法内部会调用willSet , didSet
,并且在willSet
调用完之后才真正赋值.
属性观察器之所以要这么设计就是因为要调用willSet
和didSet
.达到监听属性改变的效果.因为inout
参数就是引用传递.如果直接把width
的地址传给test()
,test
内部就直接修改了width
的值.willSet
和didSet
根本就不会触发.
现在我们总结一下inout
:
-
inout
本质就是引用传递 - 当
inout
参数是计算属性或者设置了属性观察器的存储属性时,采取了copy in , copy out
的做法:
2.1: 调用函数时先复制参数的值,产生副本( copy in )
2.2: 将副本的内存地址传入函数,在函数内修改的是副本的值
2.3: 将副本的值取出来,覆盖实参的值( copy out )
六: 类型属性
上面讲的属性都是实例属性,通过实例访问的.Swift 中还有通过类型访问的属性--类型属性.
类型属性通过static
关键字定义;如果是类,也可以通过class
关键字定义.
//类型属性
struct Person{
static var age: Int = 1
}
Person.age = 10
类型属性的本质就是全局变量,在整个程序运行过程中,只有1份内存.
我们用汇编看一下以下代码num1 , age , num2
的内存地址:
var num1 = 10
struct Person{
static var age: Int = 1
}
Person.age = 11
var num2 = 12
汇编如下:


会发现num1 , age , num2
三个变量的地址都是连续的,说明他们都在全局区.
类型属性还有个很重要的特性:类型属性默认是 lazy , 在第一次使用的时候才会初始化, 并且是线程安全的,只会初始化一次.
前面我们讲延迟存储属性 lazy 关键字
时说过,lazy
不是线程安全的.为什么类型属性默认是lazy
,它为什么是线程安全的呢?
因为它的内部会调用swift_once``dispatch_once_f
下面我们通过汇编证明一下,首先断点打到类型属性初始化的部分,看看类型属性初始化的函数地址.

然后运行程序,看到类型属性初始化函数地址为:

接着把断点调整到如图所示位置:

运行代码,分析汇编如下:

进入函数:

进入swift_once
:

会发现swift_once
内部会调用dispatch_once_f
.
所以现在就能明白为什么类型属性是线程安全的了,因为它的初始化代码放到dispatch_once_f
中调用的.