Swift学习(三)类与结构体的属性

250 阅读4分钟

一、存储属性-常量(let)VS 变量(var)

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由var关键字引入)要么是常量存储属性(由let关键字引入)。存储属性这里没有什么特别要强调的,因为随处可见

let和var

class OSLetvarPorperty {
    let x: Double = 0.0

    var y: Double = 0.0
}

WX20220102-134756@2x.png

SIL比较

WX20220102-140239@2x.png

结论

常量和变量在内存方面没有区别,let常量不能被修改的原因是编译器没有为其自动生成set方法。

二、计算属性

存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供getter和setter来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。 计算属性的根本就是set、get方法.

class OSSquera {
    //存储属性
    var width: CGFloat = 0.0
    //计算属性
    var area: CGFloat {
        get {
             return width * width
        }
        set {
            width = newValue
        }
    }

    //只读计算属性
    var area1: CGFloat {
        get {
            // return 可以省略
            width * width
        }
    }

    //set方法只有在该类内访问。根本还是存储属性
    private(set) var area2: CGFloat = 0.0
}

观察其SIL

WX20220102-133917@2x.png

会发现area和area1没有@_hasStorage @_hasInitialValue 关键字,说明并没有内存存储情况。

三、观察属性

属性观察者会观察属性值的变化,一个willSet当属性将被改变调用,即使这个值与原有的值相同,而didSet在属性已经改变之后调用。它们的语法类似于getter和setter。

class OSObserveProperty {
    var id : String  = "" {
        willSet {
            print("id will be set!")
        }
        didSet {
            print("id has been set!")
        }
    }
}

需要注意的是,初始化期间不会调用willSet 和 didSet,在初始化的过程中,是直接拿到属性的内存地址进行赋值,没有通过set方法。在初始化以下类的时候并没有出现打印。

class OSObserveProperty {
    var id : String  {
        willSet {
            print("id will be set!")
        }
        didSet {
            print("id has been set!")
        }
    }
     init(_ id: String) {
        self.id = id
    }
}

观察属性值能观察存储属性,对计算属性不起作用,计算属性也用不着,可以直接在set或者get方法中监听即可

继承情况下,同一属性的willSet和didSet在子类和父类的调用顺序是先调用子类的willSet,再调用父类的willSet,再调用父类的didSet,最后调用子类的didSet。

class OSObservePropertySub: OSObserveProperty {
    override var id: String {
        willSet {
            print("subClasss id will be set!")
        }
        didSet {
            print("subClasss id has been set!")
        }
    }

    override init(_ id: String) {
        super.init(id)
    }
}
    let subO = OSObservePropertySub("002")
    subO.id = "003"

打印:

WX20220102-133032@2x.png

四、延迟存储属性

延迟存储属性的初始值在其第一次使用时才进行计算。用关键字lazy来标识一个延迟存储属性。

代码如下:

class OSLazyProperty {
    lazy var height: Int  = 20
}

在执行过程中使用断点调试观察其内存

WX20220102-130034@2x.png 在没有调用height属性之前,lazy对象应该存储height的内存是清零的。而在调用完之后,这块内存存入了0x14(20)值。

注意:延迟加载属性必须有初始值。这一点是为了确保swift的安全性。

五、类型属性

类型属性用关键字static修饰

class OSStaticProperty {
    static var userId = "000001"
}

观察其SIL

WX20220104-103311@2x.png 多了个全局变量,对其改变并生成SIL观察

class OSStaticProperty {

    static var userId = "000001"

    func setUserId() {
        OSStaticProperty.userId = "11111"
    }
}

WX20220104-110629@2x.png 再全局搜索该方法名。

WX20220104-110103@2x.png 可以看到once关键词,这个是sil中的一次性函数 可以在swift源码中搜索swift_once WX20220104-105024@2x.png

从这里我们可以衍生出swift中单例的写法。

class OSStaticProperty {
    //单例的写法
    static let manager = OSStaticProperty()
}

进一步优化,我们可以把置顶初始化器设置为私有属性,这样外部只能访问manager来获取对象。

class OSStaticProperty {
    //单例的写法
    static let manager = OSStaticProperty()
    
    private init(){}

注意:类型变量是可以被继承的。

六、属性Moch-O文件中的位置

在之前的文章我们在Mach-o找到了方法存储的地址。这次我们用类似的方法来寻找属性的虚拟地址。先来回顾一下Metadate的数据结构。

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}

在之前的文章中我们认识了typeDescriptor,这里面记录了V-table的相关信息,接下来我们需要认识一下typeDescriptor中的fieldDescriptor

struct TargetClassDescriptor{
    var flags:UInt32
    var parent:UInt32
    var name:Int32
    var accessFunctionPointer:Int32
    var fieldDescriptor:Int32
    var superClassType:Int32
    var metadataNegativeSizeInWords:UInt32
    var metadataPositiveSizeInWords:UInt32
    var numImmediateMembers:UInt32
    var numFields:UInt32
    var fieldOffsetVectorOffset:UInt32
    var Offset:UInt32
    var size:UInt32
    //V-Table
}

fieldDescriptor记录了当前的属性信息,其中fieldDescriptor在源码中的结构如下

struct FieldDescriptor{
    MangledTypeName int32
    Superclass  int32
    Kind    uint16
    FieldRecordSize uint16
    NumFields   uint32
    FieldRecords [FieldRecord]
}

其中NumFields代表当前有多少个属性,FieldRecords记录了每个属性的信息,FieldRecord的结构如下:

struct FieldRecord{
    Flags uint32
    MangledTypeName int32
    FieldName int32
}