Swift -- 06 属性

475 阅读10分钟

swift.webp

属性

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

sizepointsea 都可以统称为实例;

关于存储属性,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定义的,结构体内存不可修改,所以两两矛盾;

属性观察器

可以为非lazyvar 存储属性设置属性观察器;有点类似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、在初始化器设置属性值,是不会触发willSetdidSet函数;
2、在属性定义的时候设置初始值,也不会触发willSetdidSet函数;
3、计算属性没有属性观察器计算属性本身就有setget函数,如果需要观察计算属性的变化,可以通过setget函数即可;

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(_ numinout 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的本质就是引用传递\color{#9B0509}{引用传递};(地址传递)

类型属性

严格来说,属性可以分为:
1、实例属性\color{#9B0509}{1、实例属性}:只能通过实例去访问

  • 存储属性:存储在实例的内存中,每个实例都有1份;
  • 计算属性

2、类型属性\color{#9B0509}{2、类型属性}:只能通过类型去访问

  • 存储类型属性:整个程序运行中,只有一份内存,类似全局变量;
  • 计算类型属性

可以通过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寄存器中; iShot_2022-06-16_10.25.03.jpg 通过register read rax指令,获取rax的内存地址;

iShot_2022-06-16_10.28.26.jpg

得出结果,三者的内存地址也是连续的:

num1内存地址:0x10000C1D0
Car.width内存地址:0x10000c1D8
num3内存地址:0x10000C1E0

结论:
static声明的变量,相当于声明了一个全局变量;

static是如何做到全局唯一,只声明一次????
Car.width第一次调用的地方打一个断点,查看汇编流程:
进入swift_beginAccess方法内 iShot_2022-06-16_11.02.25.jpg

进入swift::runtime::SwiftTLSContext::get()函数 iShot_2022-06-16_11.05.10.jpg

swift::runtime::SwiftTLSContext::get()函数内,系统调用dispatch_once_f函数; iShot_2022-06-16_11.45.34.jpg dispatch_once_f函数可以保证static声明的属性,只初始化一次,并且是线程安全的;