美文网首页
swift基础_属性

swift基础_属性

作者: 李永开 | 来源:发表于2022-02-09 16:18 被阅读0次

    一.存储属性&计算属性

    • swift属性分为两大类 stored property(存储属性) computed property(计算属性)
    • stored property:属性放在该对象内部,占用该对象内存,所以只有struct和class才有存储属性,枚举就不可以有存储属性,因为关联值和原始值都放在枚举类里面,而不是枚举实例化对象
    • computed property: 只用作计算,没有真正存储到实例化对象的内存中
    • 所以,一个对象占用内存的大小,只计算 stored property(存储属性) ,而不计算computed property(计算属性)
    //下面的storedP就是存储属性, 而computedP为计算属性(本质是方法)
            class Property{
                var storedP : Int = 0 //存储属性要初始化值才可以编译
                var computedP: Int {
                    get{
                        storedP * 2
                    }
                    set{
                        storedP = 9
                    }
                }
            }
            let p = Property()
            p.storedP = 11
            print(p.computedP)                          //22
            print(MemoryLayout<Property>.size)          //8
            print(MemoryLayout<Property>.stride)        //8
            print(class_getInstanceSize(Property.self)) //24  类(HeapObject)16+Int8
    

    可以看到计算属性并不占用内存空间

    二. 属性观察器

    2.1 什么是属性观察器

            class PropertyWillDidSet {
                var name: String = "water" {
                    willSet{
                        print("willSet \(newValue)")
                    }
                    didSet{
                        print("didSet \(oldValue)")
                    }
                }
            }
            let p = PropertyWillDidSet()
            p.name = "fire"
            
            //willSet fire
            //didSet water
    
    • 顾名思义,属性观察器就是用来观察属性变化的,属性观察器可以将新值和老值都获取到
    • 使用swiftc -emit-sil sil.swift > sil.sil拿到这段代码的sil
      发现在PropertyWillDidSet的name的set方法中,调用了name的willsetdidset方法
    图片.png

    2.2 init方法里面调用set

    下面这段代码,自定义初始化器内部调用set方法,会调用属性观察器的willsetdidset方法吗?

    class PropertyWillDidSet {
        var name: String = "water" {
            willSet{
                print("willSet \(newValue)")
            }
            didSet{
                print("didSet \(oldValue)")
            }
        }
        init() {
            self.name = "trump"
        }
    }
    let p = PropertyWillDidSet()
    

    答案是Flase
    在init方法里面调用set方法,是不会触发属性观察器的。
    为什么呢?因为你现在还在init方法中,这时的init还没有真正的初始化完成,swift不允许这样不安全的操作,所以不会触发属性观察器。


    那么下面的代码呢?

    class Father {
        var name: String = "water" {
            willSet{
                print("Father willSet \(newValue)")
            }
            didSet{
                print("Father didSet \(oldValue)")
            }
        }
    }
    class Son: Father {
        override init() {
            super.init()
            self.name = "SonName"
        }
    }
    let son = Son()
    
    //Father willSet SonName
    //Father didSet water
    

    可以看到在init方法里面调用了父类的属性观察器。这是因为super.init后,该类已经初始化完成,现在的赋值操作是安全的,所以可以触发。

    2.3 override父类的属性观察器

    class Father {
        var name: String = "water" {
            willSet{
                print("Father willSet \(newValue)")
            }
            didSet{
                print("Father didSet \(oldValue)")
            }
        }
    }
    class Son: Father {
        override var name: String {
            willSet{
                print("Son willSet \(newValue)")
            }
            didSet{
                print("Son didSet \(oldValue)")
            }
        }
    }
    let son = Son()
    son.name = "老王"
    
    //打印如下:
    Son willSet 老王
    Father willSet 老王
    Father didSet water
    Son didSet water
    
    • 正常来说,覆写父类的方法应该只会调用子类自己的方法,为什么还会调用Father的willSet和didSet呢?

    2.4 什么样的属性可以有属性观察器呢

    1. 存储属性
    2. 继承的存储属性
    3. 继承的计算属性
    • 总之就是:属性观察器不能和get&set方法同时存在,也就是说willSet&didSet不能和set方法同时存在,编译器会报错的。

    三. 延迟存储属性 lazy stored property

    • 使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会初始化
    class LazyClass {
        init() {
            print("LazyClass初始化啦")
        }
        func lazyRun() {
            print("Lazy运行")
        }
    }
    class Person {
        lazy var l = LazyClass()
        init() {
            print("Person初始化啦")
        }
        
        func useLazy () {
            l.lazyRun()
        }
    }
    let person = Person()
    person.useLazy()
    打印:
    //Person初始化啦
    //LazyClass初始化啦
    //Lazy运行
    

    可以看到,虽然LazyClass先创建的,但是没有调用它的init方法。而是在第一次使用的时候才调用它的init方法。

    3.1 lazy特点
    import Foundation
    
    class PersonTest {
        var age: Int = 11
    }
    let p = PersonTest()
    print(class_getInstanceSize(PersonTest.self)) //24
    
    import Foundation
    
    class PersonTest {
        lazy var age: Int = 11
    }
    let p = PersonTest()
    print(class_getInstanceSize(PersonTest.self)) //32
    

    我们给PersonTest加上lazy以后,发现该类占用的内存增大了。
    我们可以通过sil来看下是怎么回事

    • 不加lazy的实现
      swiftc -emit-sil sil.swift | xcrun swift-demangle > sil.sil
    class PersonTest {
      @_hasStorage @_hasInitialValue var age: Int { get set }
      @objc deinit
      init()
    }
    // PersonTest.age.getter
    sil hidden [transparent] @sil.PersonTest.age.getter : Swift.Int : $@convention(method) (@guaranteed PersonTest) -> Int {
    // %0 "self"                                      // users: %2, %1
    bb0(%0 : $PersonTest):
      debug_value %0 : $PersonTest, let, name "self", argno 1 // id: %1
      %2 = ref_element_addr %0 : $PersonTest, #PersonTest.age // user: %3
      %3 = begin_access [read] [dynamic] %2 : $*Int   // users: %4, %5
      %4 = load %3 : $*Int                            // user: %6
      end_access %3 : $*Int                           // id: %5
      return %4 : $Int                                // id: %6
    } // end sil function 'sil.PersonTest.age.getter : Swift.Int'
    
    // PersonTest.age.setter
    sil hidden [transparent] @sil.PersonTest.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed PersonTest) -> () {
    // %0 "value"                                     // users: %6, %2
    // %1 "self"                                      // users: %4, %3
    bb0(%0 : $Int, %1 : $PersonTest):
      debug_value %0 : $Int, let, name "value", argno 1 // id: %2
      debug_value %1 : $PersonTest, let, name "self", argno 2 // id: %3
      %4 = ref_element_addr %1 : $PersonTest, #PersonTest.age // user: %5
      %5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
      store %0 to %5 : $*Int                          // id: %6
      end_access %5 : $*Int                           // id: %7
      %8 = tuple ()                                   // user: %9
      return %8 : $()                                 // id: %9
    } // end sil function 'sil.PersonTest.age.setter : Swift.Int'
    
    • 加了lazy的实现
    class PersonTest {
      lazy var age: Int { get set }
      @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
      @objc deinit
      init()
    }
    // PersonTest.age.getter
    sil hidden [lazy_getter] [noinline] @sil.PersonTest.age.getter : Swift.Int : $@convention(method) (@guaranteed PersonTest) -> Int {
    // %0 "self"                                      // users: %14, %2, %1
      switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6
    }
    

    可以看到,加了lazy的属性,系统会将其变成可选型。在你未访问它之前,他默认值为nil;访问它以后,它才会赋值
    我们看下可选型的大小,发现Optional<Int>为16, int为8 ,这也就是为什么加了lazy以后多了8个字节。

    print(MemoryLayout<Optional<Int>>.size)//9
    print(MemoryLayout<Optional<Int>>.stride)//16
    

    四. Type property 类型属性

    struct TypeProperty {
        var width : Int = 0
        static var height : Int = 0
    }
    let typeP = TypeProperty()
    typeP.width
    TypeProperty.height;
    
    • 使用static修饰的属性为类型属性。类型属性,只能通过类型去访问
    • 使用static修饰的属性为全局变量,存放在全局区,只有一份内存
    // static TypeProperty.height
    sil_global hidden @static sil.TypeProperty.height : Swift.Int : $Int
    
    • 类型属性是线程安全的
    // one-time initialization token for height
    sil_global private @one-time initialization token for height : $Builtin.Word
    

    初始化height的时候会调用swift_once,swift_once ->底层调用了gcd的dispatch_once_f

    • 所以,swift单例可以这样写
    class ShareInstance {
        static let share: ShareInstance = ShareInstance() //static保证了share只被创建了一次
        private init(){} //禁止使用init方法
    }
    

    FinalQuestion

    • 要lazy到底有啥用?
      节约资源 虽然你代码写了初始化一个类,并且也调用了该类的init方法,但是我暂时不会执行;等你真正用的时候我才去初始化,延迟加载了就节省资源了。
    • swift的init方法里面做了什么,为什么在init方法里面赋值不会触发属性观察器
    • 覆写父类的方法为什么还能调用父类的方法,可不可以不调用父类的方法
    • lazy属性,如果把age变成class,你会发现大小没有变,那是因为print(MemoryLayout<Optional<LazyClass>>.size)//8 print(MemoryLayout<Optional<LazyClass>>.stride)//8,那么为什么类的可选型是8,而int的可选型为16呢。
         size的作用:
         并不能看出该类型在内存中实际使用的大小,因为size并不包含任何动态分配
         而且 如果是class类型,那么不管用多少存储属性,他们的size都是一样的,占用8个字节
         
         stride的作用:
         对象实际在内存中占用的大小,经过了内存对齐,它的大小永远是份分配积极的,只多不少
    
        // 结构体没有继承,它的内存大小是由它内部的存储变量决定的
        // 类有继承,它的size永远是8
    
    • 为什么延迟存储属性必须要初始化呢?
      是因为本质是可选型吗?
    • 为什么延迟存储属性不是线程安全的呢?
      多个线程同时访问延迟属性时,由于lazy_property内部存在可选判断会执行多个分支,不加锁当然就不是线程安全的喽。。。你看lazy var cls = LazyClass(),其中LazyClass就可能被创建多次

    相关文章

      网友评论

          本文标题:swift基础_属性

          本文链接:https://www.haomeiwen.com/subject/bdnwkrtx.html