Swift中跟实例相关的属性可以分为2大类
- 1、存储属性
- 类似于成员变量这个概念
- 存储在实例的内存中
- 结构体,类可以定义存储属性
- 枚举不可以定义存储属性
- 2、计算属性
- 本质就是一个函数
- 不占用实例的内存
- 枚举,结构体,类都可以定义计算属性
struct Circle {
var radius:Double
var diameter:Double{
set{
radius = newValue / 2
}
get{
radius * 2
}
}
}
print(MemoryLayout.size) //8
print(MemoryLayout.size) //8
radius
是存储属性,diameter
是计算属性,我们打印Circle
的内存大小发现和Double
一样
存储属性
关于存储属性,Swift有一个明确的规定
- 在创建类或者结构体实例时,必须为所有的存储属性设置一个合适的初始值
- 可以在初始化器里为存储属性设置一个初始值
- 可以分配一个默认的属性值作为属性定义的一部分
计算属性
除了存储属性,类、结构体和枚举也能够定义计算属性,而它实际并不存储值。相反,他们提供一个读取器和一个可选的设置器来间接得到和设置其他的属性和值
struct Circle {
var radius:Double
var diameter:Double{
set{
radius = newValue / 2
}
get{
radius * 2
}
}
}
diameter
就是一个简单的计算属性,如果一个计算属性的设置器没有为将要被设置的值定义一个名字,那么他将被默认命名为 newValue
只读计算属性:只有get方法,没有set方法
- 定义计算属性只能使用
var
,不能使用let
-
let
代表常量:值是不变的 - 计算属性的值是可能发生变化的(即使是只读属性)
struct Circle {
var radius:Double
var diameter:Double{
get{
radius * 2
}
}
}
var c = Circle(radius: 10)
print(c.diameter) //20
c.radius = 5
print(c.diameter) //10
枚举rawValue的原理
我们研究发现,其实枚举原始值中rawValue
的本质就是:只读计算属性
enum TestEnum:Int {
case test1 = 1, test2 = 2, test3 = 3
}
print(TestEnum.test1.rawValue) //1
对于这样的我们打印就是1
enum TestEnum:Int {
case test1 = 1, test2 = 2, test3 = 3
var rawValue:Int{
switch self {
case .test1:
return 10
case .test2:
return 20
case .test3:
return 30
}
}
}
print(TestEnum.test1.rawValue) //10
对于这个打印就是10
延迟存储属性
使用lazy
可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
你必须把延迟存储属性声明为变量(使用 var 关键字),因为它的初始值可能在实例初始化完成之前无法取得。常量属性则必须在初始化完成之前有值,因此不能声明为延迟
如果多条线程同时第一次访问lazy
属性,无法保证属性只能被初始化一次
class PhotoView {
lazy var image:UIImage = {
let url = "xxx.png"
let data = Data(url)
return UIImage(data:data)
}
}
当结构体包含一个延迟存储属性时,只有var
才能访问演出属性
- 因为延迟属性初始化时需要改变结构体的内存
属性观察器(Property Observer)
属性观察者会观察并对属性值的变化做出回应。每当一个属性的值被设置时,属性观察者都会被调用,即使这个值与该属性当前的值相同
。
你可以为你定义的任意存储属性添加属性观察者,除了延迟存储属性
。你也可以通过在子类里重写属性来为任何继承属性(无论是存储属性还是计算属性)添加属性观察者
你不需要为非重写的计算属性定义属性观察者,因为你可以在计算属性的设置器里直接观察和相应它们值的改变。
- willSet 会在该值被存储之前被调用。
- didSet 会在一个新值被存储后被调用。
如果你实现了一个 willSet
观察者,新的属性值会以常量形式参数传递。你可以在你的 willSet
实现中为这个参数定义名字。如果你没有为它命名,那么它会使用默认的名字newValue
同样,如果你实现了一个 didSet
观察者,一个包含旧属性值的常量形式参数将会被传递。你可以为它命名,也可以使用默认的形式参数名 oldValue
。如果你在属性自己的 didSet
观察者里给自己赋值,你赋值的新值就会取代刚刚设置的值。
- 初始化器中设置属性值的时候不会触发
willSet 和 didSet
- 在属性定义时设置初始值也不会触发
willSet 和 didSet
struct Circle {
var radius:Double{
willSet{
print("willSet",newValue)
}
didSet{
print("didSet",oldValue)
}
}
}
inout再次研究
struct Shape {
var width:Int
var side:Int {
willSet{
print("willSet",newValue)
}
didSet{
print("didSet",oldValue)
}
}
var grith:Int{
set{
width = newValue / side
print("setGrith",newValue)
}
get{
print("getGrith")
return width * side
}
}
func show() {
print("width:\(width)--side:\(side)--grith:\(grith)")
}
}
func test (_ num: inout Int){
print("test")
num = 20
}
var s = Shape(width: 10, side: 4)
s.show()
print("-------")
test(&s.width)
s.show()
print("-------")
test(&s.grith)
s.show()
print("-------")
test(&s.side)
s.show()
print("-------")
我们对这个进行分析
1、在test(&s.width)
处打断点
struct Shape {
var width:Int
var side:Int {
willSet{
print("willSet",newValue)
}
didSet{
print("didSet",oldValue)
}
}
var grith:Int{
set{
width = newValue / side
print("setGrith",newValue)
}
get{
print("getGrith")
return width * side
}
}
func show() {
print("width:\(width)--side:\(side)--grith:\(grith)")
}
}
func test (_ num: inout Int){
print("test1")
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.width)
想要具体的分析,我们可以查看汇编实现。提前需要了解的知识点
1、汇编中
callq
表示函数的调用,我们先找到callq 0x1000025a0 ; 属性.test(inout Swift.Int) -> ()
这句话,表示调用函数test
-
2、
leaq 0x1abe(%rip), %rdi ; 属性.s : 属性.Shape
我们向上看一句,这一句表示传递参数- 在汇编语言中
rdi、rsi、rdx、rcx、r8、r9等寄存器常用于存放函数参数
,这里我们找到了rdi
-
leaq 0x1abe(%rip), %rdi
在汇编语言中的意思就是:将0x1abe(%rip)
这个地址值赋值给了rdi
-
leaq 0x1abe(%rip), %rdi ; 属性.s : 属性.Shape
我们可以看到后面的注释,是把s
的地址给了rdi
- 我们知道
结构体第一个属性的地址值
就是结构体的地址
,所有rip
就是存放的width
的地址
- 在汇编语言中
2、在test(&s.grith)
处打断点
struct Shape {
var width:Int
var side:Int {
willSet{
print("willSet",newValue)
}
didSet{
print("didSet",oldValue)
}
}
var grith:Int{
set{
width = newValue / side
print("setGrith",newValue)
}
get{
print("getGrith")
return width * side
}
}
func show() {
print("width:\(width)--side:\(side)--grith:\(grith)")
}
}
func test (_ num: inout Int){
print("test1")
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.grith)
我们知道inout
本质就是引用传递
,而在上面我们知道计算属性本质就是一个函数
,s.grith
没有内存地址,这里为什么不报错呢?我们先来看一下打印结果
getGrith
test
setGrith 20
我们先大概猜测一下实现原理
- 1、首先调用
get
方法,他会返回一个临时的存储空间,也就是局部变量 - 2、把临时的存储空间的地址值传递到
test
函数中去,test
函数能够拿到临时的存储空间,然后把20放到临时的存储空间中 - 3、把20传到
set
方法的newValue
中
我们来看一下汇编实现
【需要了解的小知识】
- 内存地址格式为:0X4dbc(%rip)、一般是全局变量、在数据段
- 内存地址格式为:-0X78(%rbp)、一般是局部变量,栈空间
- 内存地址格式为:0X10(%rax)、一般是堆空间
- 1、我们来看
movq %rax, -0x28(%rbp)
,这个是第24行代码,意思就是把get方法的返回值放到-0x28(%rbp)
的局部变量中 - 2、第25-26行
leaq -0x28(%rbp), %rdi
把-0x28(%rbp)
赋值给%rdi
,然后把%rdi
作为参数传递到test
方法中 - 3、
movq -0x28(%rbp), %rdi
,取-0x28(%rbp)
的内存地址值给%rdi
,然后把%rdi
传递给set
方法
3、在test(&s.side)
处打断点
struct Shape {
var width:Int
var side:Int {
willSet{
print("willSet",newValue)
}
didSet{
print("didSet",oldValue)
}
}
var grith:Int{
set{
width = newValue / side
print("setGrith",newValue)
}
get{
print("getGrith")
return width * side
}
}
func show() {
print("width:\(width)--side:\(side)--grith:\(grith)")
}
}
func test (_ num: inout Int){
print("test1")
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.side)
inout本质总结
- 如果实参有物理内存地址,且没有属性观察器,直接将实参的内存地址传入函数(实参进行引用传递)
- 如果实参是计算属性或者设置了属性观察器,采用了
Copy in Copy Out
的做法- 调用该函数时,先复制实参的值,产生副本(get)
- 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
- 函数返回后,再将副本的值覆盖实参的值(set)
**inout的本质就是引用传递(地址传递)**
类型属性
例属性是属于特定类型实例的属性。每次你创建这个类型的新实例,它就拥有一堆属性值,与其他实例不同。
你同样可以定义属于类型本身的属性,不是这个类型的某一个实例的属性。这个属性只有一个拷贝,无论你创建了多少个类对应的实例。这样的属性叫做类型属性。
类型属性在定义那些对特定类型的所有实例都通用的值的时候很有用,比如实例要使用的常量属性(类似 C 里的静态常量),或者储存对这个类型的所有实例全局可见的值的存储属性(类似 C 里的静态变量)。
存储类型属性可以是变量或者常量。计算类型属性总要被声明为变量属性,与计算实例属性一致。
注意
不同于存储实例属性,你必须总是给存储类型属性一个默认值。这是因为类型本身不能拥有能够在初始化时给存储类型属性赋值的初始化器。
存储类型属性是在它们第一次访问时延迟初始化的。它们保证只会初始化一次,就算被多个线程同时访问,他们也不需要使用 lazy 修饰符标记。
严格来说,属性可以分为
- 1、实例属性:只能通说实例去访问
- 计算属性:存储在实例的内存中,每个实例都有一份
- 存储属性
- 2、类属性:只能通过类型去访问
- 存储属性:整个程序运行中,就只有1份
- 计算属性
类属性可以通过static
定义属性,
类型属性细节
- 1、不同于
存储实例属性
,你必须给存储类型属性
设置初始值,因为类型没有像实例那样有init
方法 - 2、
存储类型属性
默认就是lazy
会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次
下面我们证明两个问题
- 1、类属性是全局变量
- 2、类属性实例化的时候调用了
dispath_once
类属性是全局变量
测试1
我们先来研究直接赋值的 num1 num2 num3
var num1 = 10
var num2 = 11
var num3 = 12
print("-------")
我们在print
出打断点,然后进入汇编
我们计算出地址值:
num1 0x100001188
num2 0x100001190
num3 0x100001198
是一段连续的栈空间地址。
测试2
接下来我们把num2
设置为类属性
struct Test {
static var num = 11
}
var num1 = 10
var num2 = Test.num
var num3 = 12
print("----")
我们在print
出打断点,然后进入汇编
我们计算出地址值:
num1 0x1000021D0
num2 0x1000021D8
num3 0x1000021E0
也是一段连续的栈空间地址。
struct Test {
var num = 11
}
var num1 = 10
var num2 = Test().num
var num3 = 12
print("----")
测试3
我们把num2
设置为实例属性
struct Test {
var num = 11
}
var num1 = 10
var num2 = Test().num
var num3 = 12
print("----")
我们来打印一下num2 num3
的地址值
num2 0x000000000000000b
num3 0x1000021A8
我们可以看出实例属性是在堆空间
类属性实例化的时候调用了dispath_once
struct Test {
static var num = 11
}
var num1 = Test.num
我们在static var num = 11
处打断点
我们在swift_once
处打断点,然后si
进入,一直si
,知道进入到
在上面汇编中的最后一句话打一个断点,继续si
最终找到了dispath_once
,类属性保持只创建了一次就是调用了dispath_once
方法