属性
Swift中跟实例
相关的属性
,分为两大类:
1、存储属性
- 类似于成员变量;
- 存储在实例的内存中;
- 结构体、类可以定义存储属性;
- 枚举
不可以
定义存储属性;因为枚举的内存是用来存储枚举的case,或者是case的关联值;
实例一般指:结构体,类,枚举等
class Size{
}
struct Point {
}
enum Season {
case test
}
//size 是 对象
var size = Size()
//point 结构体变量
var point = Point()
//sea 枚举变量
var sea = Season.test
size、point、sea 都可以统称为实例;
关于存储属性,Swift有明文规定:
在创建类
或结构体
的实例时,必须为所有的
存储属性设置初始值;
1、可以在初始化器里为存储属性设置初始值;
2、可以分配默认的属性值作为初始值;
2、计算属性
- 本质就是方法(函数)
- 不占用实例的内存
- 枚举、结构体、类都可以定义计算属性
struct Circle {
var radius:Int
var diameter:Int
{
get{
radius * 2
}
set {
radius = newValue/2
}
}
}
var c = Circle(radius: 12)
c.radius = 30
c.diameter = 18
//结果:8,c结构体变量,占用8字节;
//说明了,diameter是计算属性,不占用实例的内存;
print(MemoryLayout.stride(ofValue: c))
set
传入的新值默认叫做newValue
,也可以自定义;
定义计算属性只能用var
,不能用let
;
let
代表常量,值不变;
计算属性的值是可以发送变化的;
枚举rawValue原理
枚举原始值rawValue
的本质是:只读计算属性;
枚举的内存是用来存储枚举的case,或者是case的关联值;
enum Season:Int{
case spring=1,summer,autumn,winner
}
var s = Season.summer
print(s.rawValue) // 结果:2
enum 的rawValue,其实就相当于在枚举内部,定义了一个只读的计算属性
;
enum Season:Int{
case spring=1,summer,autumn,winner
var rawValue: Int{
switch self {
case .spring:
return 11
case .summer:
return 22
case .autumn:
return 33
case .winner:
return 44
}
}
}
var s = Season.summer
print(s.rawValue)//结果:22
延迟存储属性
使用lazy
可以定义一个延迟存储属性
,在第一次用到属性的时候才会初始化;\
1、未使用lazy
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()
执行流程:
初始化p对象
1、先执行car对象的初始化,调用Car的init()构造函数;
2、再调用Person的init()构造函数;
3、执行分割线print;
4、执行p对象的goOut函数
2、使用lazy
class Person
{
lazy var car = Car()
init(){
print("person init")
}
func goOut(){
car.run();
}
}
执行流程:
初始化p对象
1、先调用Person的init()构造函数;
2、再执行分割线print;
3、调用p对象的goOut函数时,先调用car对象的init()构造函数,再执行goOut函数;
3、注意事项
1、lazy
属性必须是var
,不能是let
;因为let
定义的属性,必须在实例初始化完成之前有值;
2、如果多条线程同时第一次访问lazy
属性,无法保证属性只初始化一次;所以lazy
不是线程安全的;
3、当结构体内部包含lazy
延迟属性时,只有var
定义的结构体变量才能访问延迟属性
;
例子:
struct Point{
var x = 0
var y = 0
lazy var z = 0
}
let p = Point()
print(p.z) //此处将报错误
说明:
p 是由let声明的,表示p结构体 的内存不可修改;
lazy 是延迟属性,只有用到的时候,才会赋值;
那么执行print(p.z)的时候,第一次调用z,则会启动var z = 0赋值流程,将会修改p结构体里z属性的内存;
但是又由于p结构体是let定义的,结构体内存不可修改,所以两两矛盾;
属性观察器
可以为非lazy
的var 存储属性
设置属性观察器;有点类似Objective-C的KVO观察;
struct Circle{
var raduis:Double{
willSet{//即将发生改变
print("willSet ",newValue)//newValue 是默认参数,表示传递进来的数据
}
didSet{//已经发生改变
print("didSet ",oldValue,raduis)
}
}
init(){
self.raduis = 1.0
print("Circle init")
}
}
var c = Circle()
//执行willSet,结果---> 12
//执行didSet 结果---> 1,12
c.raduis = 12
print(c.raduis)
注意:
1、在初始化器
设置属性值,是不会触发willSet
、didSet
函数;
2、在属性定义
的时候设置初始值,也不会触发willSet
、didSet
函数;
3、计算属性
没有属性观察器
,计算属性
本身就有set
、get
函数,如果需要观察计算属性
的变化,可以通过set
、get
函数即可;
inout 输入输出参数的本质
inout
输入输出参数,可用于在函数内部,修改外部实参的值;
它的本质是引用传递
、引用类型
;
通过例子和汇编,来讲解inout
的本质
struct Shape {
//宽
var width : Int//存储属性
//角的数量
var size : Int
{//存储属性
//willSet、didSet 属性观察器
willSet{
print("size willSet",newValue)
}
didSet
{
print("size didSet",oldValue,size)
}
}
//周长
var girth :Int
{//计算属性
set{
print("setGirth")
width = newValue/size
}
get{
print("getGirth")
return width * size
}
}
func show(){
print("width = \(width),size = \(size)")
}
}
/*
inout 输入输出参数,可在函数内部,修改外部实参的值;
引用类型,本质是地址传递
*/
func test(_ num: inout Int){
print("test")
num = 20
}
var s = Shape.init(width: 10, size: 4)
test(&s.width)
test(&s.girth)
test(&s.size)
s.show()
test(&s.width)汇编讲解
0x100002dae <+62>: leaq 0x941b(%rip), %rdi ; QLYTestSwift.s : QLYTestSwift.Shape
0x100002db5 <+69>: callq 0x100003770 ; QLYTestSwift.test(inout Swift.Int) -> () at main.swift:561
rdi 代表:函数参数;
leaq 地址传递汇编语句
leaq 0x941b(%rip), %rdi
将 0x941b(%rip) 这个全局变量的地址,传递到rdi。
从右侧的解释可以看出,0x941b(%rip) 是 s 这个全局变量的地址值,传递给 rdi;
写的是 &s.width,为什么传递的却是s 变量的地址值;
s.width是 s变量的第一个属性,所以width的地址,就是s变量的首地址;
test(&s.girth)汇编讲解
girth 是计算属性,本质上就是函数,函数是存放在堆空间,那么s实例内部,是没有girth的内存地址的;
执行test(&s.girth),先执行girth的get方法,将返回值存入一个临时局部变量中,然后通过inout,将临时局部变量的地址,传递给test函数的参数num;
test函数,可以通过这个地址,直接将20存入地址中;test函数执行完毕,执行girth的set方法,将新输入通过newValue传入
//调用getter方法,它的返回值存在rax中
0x1000028de <+78>: callq 0x100002fb0 ; QLYTestSwift.Shape.girth.getter : Swift.Int at main.swift:554
//将get方法的返回值,存放到函数栈空间-0x28(%rbp),函数的栈空间范围,是rbp~rsp指针之间
0x1000028e3 <+83>: movq %rax, -0x28(%rbp)
//将局部变量的地址值,给rdi,rdi是函数的参数,也就是将局部变量的地址值,传递给的test函数
0x1000028e7 <+87>: leaq -0x28(%rbp), %rdi
//test函数,根据局部变量地址值,将数据改为20
0x1000028eb <+91>: callq 0x100003680 ; QLYTestSwift.test(inout Swift.Int) -> () at main.swift:569
// movq,根据局部变量地址值,取出内存数据, 也就是20,然后赋给rdi,函数的参数;
0x1000028f0 <+96>: movq -0x28(%rbp), %rdi
0x1000028f4 <+100>: leaq 0x98d5(%rip), %r13 ; QLYTestSwift.s : QLYTestSwift.Shape
//调用setter函数,新赋值的数据,传递给newValue,所以newValue = 20
0x1000028fb <+107>: callq 0x100003120 ; QLYTestSwift.Shape.girth.setter : Swift.Int at main.swift:550
test(&s.size)汇编讲解
size是存储属性,可以存储在s实例内存中;
rdi是test函数的参数;
//rax存储属性返回值,从右侧的解释可以看出,rax取到的是s实例的后8个字节,也就是size内存里的数据(也就是size的值:4),然后存入rax寄存器
0x100002960 <+64>: movq 0x9871(%rip), %rax ; QLYTestSwift.s : QLYTestSwift.Shape + 8
//movq,获取rax寄存器地址里的内存数据,赋值给函数栈空间-0x28(%rbp),也就是将size的内存数据,赋值给一个局部变量
0x100002967 <+71>: movq %rax, -0x28(%rbp)
//将局部变量的地址,赋值给test函数的参数,也就是将size的地址值,赋值给test函数的num参数
0x10000296b <+75>: leaq -0x28(%rbp), %rdi
//test函数,根据地址值,将地址对应的内存数据改为20
0x10000296f <+79>: callq 0x100003680 ; QLYTestSwift.test(inout Swift.Int) -> () at main.swift:569
//将修改后的新值,赋值给函数参数
0x100002974 <+84>: movq -0x28(%rbp), %rdi
0x100002978 <+88>: leaq 0x9851(%rip), %r13 ; QLYTestSwift.s : QLYTestSwift.Shape
//调用size的属性观察器set函数,set函数内部包含willSet、didSet两个函数
0x10000297f <+95>: callq 0x100002d80 ; QLYTestSwift.Shape.size.setter : Swift.Int at main.swift:534
总结
1、如果实参有物理内存地址,且没有设置属性观察器,直接将实参的内存地址传入函数,实参进行引用传递;
2、如果实参是计算属性,或者设置了属性观察器,则采取Copy in Copy out的方式;
- 调用函数时,先复制实参的值,产生副本(get);
- 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值;
- 函数返回后,将副本的值覆盖函数参数的值;(set)
3、
inout
的本质就是;(地址传递)
类型属性
严格来说,属性可以分为:
:只能通过实例
去访问
- 存储属性:存储在
实例
的内存中,每个实例都有1份; - 计算属性
:只能通过类型去访问
- 存储类型属性:整个程序运行中,只有一份内存,类似全局变量;
- 计算类型属性
可以通过static
定义类型属性;
如果是类
,可以使用class
关键字;\
注意:
不同于存储实例属性
,必须给存储类型属性
设置初始值;因为类型属性
没有初始化构造器(不会自动生成init()函数);
存储类型属性
默认是lazy
,会在第一次使用的时候再初始化;
就算被多个线程同时访问,也只会初始化一次;
存储类型属性
可以使用let
关键字;
枚举也可以定义类型属性
;
单例
class FileManager{
//使用let,可以保证外面无法修改
//使用public,可以提供给其他类使用
public static let shared = FileManager()
private init(){
}
func open(){
}
func close(){
}
}
FileManager.shared.open()
FileManager.shared.close()
汇编探索static
定义三个全局变量,结果为:变量的内存地址是连续的;
var num1 = 10
var num2 = 11
var num3 = 12
// 变量的内存地址
0x10000C1C0
0x10000C1C8
0x10000C1D0
在全局变量之间,定义类型属性,那么内存地址又是如何变化呢?
var num1 = 10
class Car{
static var width : Int = 0
}
Car.width = 11
var num3 = 12
通过汇编$0xb,(%rax)
,可以看到系统将 11
这个值,存入了rax
寄存器中;
通过
register read rax
指令,获取rax
的内存地址;
得出结果,三者的内存地址也是连续的:
num1内存地址:0x10000C1D0
Car.width内存地址:0x10000c1D8
num3内存地址:0x10000C1E0
结论:
static
声明的变量,相当于声明了一个全局变量;
static
是如何做到全局唯一,只声明一次????
在Car.width
第一次调用的地方打一个断点,查看汇编流程:
进入swift_beginAccess
方法内
进入swift::runtime::SwiftTLSContext::get()
函数
在swift::runtime::SwiftTLSContext::get()
函数内,系统调用dispatch_once_f
函数;
dispatch_once_f
函数可以保证static
声明的属性,只初始化一次,并且是线程安全的;