美文网首页
从零学习Swift 07:属性

从零学习Swift 07:属性

作者: 小心韩国人 | 来源:发表于2020-04-28 14:17 被阅读0次
    总结

    Swift 中的属性分为两大类:存储属性 , 计算属性

    一: 存储属性

    存储属性类似于成员变量,定义方式很简单:

    //存储属性
    class Person{
        var name: String = "张三"
    }
    

    存储属性存储在成员变量中;结构体Struct和类Class都可以定义存储属性,唯独枚举不可以.因为枚举变量的内存中只用于存储case 值和关联值,没有用来存储存储属性的内存.

    另外需要注意的是,在初始化类和结构体的实例时,必须为所有的存储属性设置初始值.

    二: 计算属性

    计算属性的定义方式需要用到set , get关键字:

    //计算属性
    var age: Int{
      set{
        ...
      }
      get{
        ...
      }
    }
    
    

    像上面的age就是一个计算属性.为了更生动的理解计算属性,我们以游戏账号升级为例子.假如规则是这样:在线两个小时,游戏账号升级一级:

    
    //计算属性
    struct Game{
        
        //游戏时长,单位 : 小时
        var time: Int
        
        //游戏等级,在线两个小时升一级
        var grade: Int {
            set{
                time = newValue * 2
            }
            get{
                time / 2
            }
        }
    }
    //初始化 游戏时长为 2 个小时
    var game1 = Game(time: 2)
    print("在线 \(game1.time) 个小时, 游戏等级为 \(game1.grade) 级")
    //设置游戏等级为 30 级
    game1.grade = 30
    print("在线 \(game1.time) 个小时, 游戏等级为 \(game1.grade) 级")
    
    

    计算属性的本质就是方法,这一点我们可以通过汇编直接看出来:

    调用计算属性的 set 方法

    既然计算属性的本质是方法,就说明计算属性是不会占用实例变量的内存.因为枚举,结构体,类中都可以定义方法,所以同样也可以定义计算属性.需要注意的是,计算属性只能用var,不能用let.因为计算属性是会变化的.

    枚举rawValue的本质

    我们之前在从零学习Swift 02:枚举和可选项 中说过,枚举是不会存储原始值的,今天我们就来搞清楚枚举rawValue的本质.
    首先看一下系统默认的rawValue:

    系统默认

    我们可以写个函数,实现rawValue的功能:

    自定义 rawValue 函数

    但是很奇怪,为什么系统的rawValue没有括号(),其实它是计算属性

    rawValue 的本质就是 只读的 计算属性

    现在我们就搞清楚了rawValue的本质其实就是只读的,计算属性

    另外我们也发现,计算属性可以只有get没有set,那可不可以只有set,没有get呢?不可以,编译器会直接报错.

    有set必须要有get
    三: 延迟存储属性

    使用lazy可以定义一个延迟存储属性,在第一次使用属性的时候才会初始化.类似于 OC 中的懒加载.

    如上图只创建了person,还没有使用car,car就已经初始化了.

    我们在car前面加上lazy关键字:

    延迟存储属性
    • 使用lazy延迟存储属性时要注意一下几点:
    1. lazy属性必须是var,不能是let.因为Swift规定let必须在实例初始化方法完成之前就有值.而lazy是用到的时候才初始化,这就冲突了.
    2. lazy属性不是线程安全的,多个线程同时访问同一个lazy属性,可能不止加载一次.
    3. 当结构体包含一个延迟存储属性时,只有var修饰的实例才能访问延迟存储属性,let修饰的实例不允许访问延迟存储属性,什么意思呢?看下面一张图就知道了:
      结构体 let 实例
    四: 属性观察器

    属性观察器类似于 OC 中的 KVO,它的定义方式如下:

    属性观察器

    使用属性观察器必须满足三个条件:

    1. 必须是非lazy修饰(因为 lazy 属性是在第一次访问属性的时候才创建的,而添加属性观察器可能会打破 lazy 的机制)
    2. 必须是var变量(既然是属性观察器,肯定是观察属性值的变化,如果用 let 常量就没有任何意义了)
    3. 必须是存储属性(因为计算属性内部本来就有一个set,可以把监听代码写到set中.)

    思考一下为什么计算属性不能设置属性观察器?
    因为计算属性内部本来就有一个set ,可以把监听代码写到set中.

    五: inout 参数

    之前在从零学习Swift 01:了解基础语法中用汇编分析过inout参数,知道inout输入输出参数是引用传递.今天使用更复杂的类型更深入的研究inout参数的本质.

    示例代码:

    
    //矩形
    struct Rectangle{
        //存储属性  长
        var length: Int
        //属性观察器  宽
        var width: Int{
            willSet{
                print("newValue : ",newValue)
            }
            
            didSet{
                print("newValue : \(width) , oldValue : \(oldValue)")
            }
        }
        
        //计算属性 (计算属性不占用实例内存空间,本质是方法)
        //面积
        var area: Int{
            set{
                length = newValue / width
            }
            
            get{
                return length * width
            }
        }
        
        func show(){
            print("长方形的长 length = \(length) , 宽 width = \(width) , 面积 area = \(area)")
        }
    }
    
    var rect = Rectangle(length: 10, width: 4)
    rect.show()
    
    
    func test(_ num: inout Int){
        num = 20
    }
    
    test(&rect.length)
    rect.show()
    
    

    上面代码结构体中分别有存储属性,属性观察器,计算属性.下面我们就分别把这三种属性传入inout参数.

    1. inout参数之存储属性

    分析汇编:


    从上图可以看到,调用test()时直接把全局变量rect的地址作为参数传入进去.为什么不是把length的地址传进去呢?因为length是结构体的第一个成员,所以结构体的地址就是length的地址.这里传入rect的地址和length地址是等价的.

    1. inout参数之计算属性

    分析汇编:


    从汇编语言中可以看到,当inout参数传入的是计算属性时,在调用test()方法之前会先调用计算属性的getter方法取出值,并且把值存入栈空间;然后再调用test()方法,并且把栈空间的地址作为参数传递进去.所以在test()方法内部修改的是栈空间的值;最后再调用计算属性的setter方法,从栈空间中取出值传入setter方法,并赋值.

    图解:


    图解

    3.inout参数之属性观察器

    分析汇编:

    从上图的汇编中可以看到,inout参数是属性观察器时,内部逻辑和计算属性很相似,都是取出值放到栈空间,然后修改栈空间的值.

    为什么属性观察器不能像存储属性那样,直接传入地址,直接修改呢?因为属性观察器涉及到监听的逻辑.我们看看第三步的setter方法的汇编:

    setter 方法

    可以看到setter方法内部会调用willSet , didSet,并且在willSet调用完之后才真正赋值.

    属性观察器之所以要这么设计就是因为要调用willSetdidSet.达到监听属性改变的效果.因为inout参数就是引用传递.如果直接把width的地址传给test(),test内部就直接修改了width的值.willSetdidSet根本就不会触发.

    现在我们总结一下inout:
    1. inout本质就是引用传递
    2. inout参数是计算属性或者设置了属性观察器的存储属性时,采取了copy in , copy out的做法:
      2.1: 调用函数时先复制参数的值,产生副本 ( copy in )
      2.2: 将副本的内存地址传入函数,在函数内修改的是副本的值
      2.3: 将副本的值取出来,覆盖实参的值( copy out )
    六: 类型属性

    上面讲的属性都是实例属性,通过实例访问的.Swift 中还有通过类型访问的属性--类型属性.
    类型属性通过static关键字定义;如果是类,也可以通过class关键字定义.

    
    //类型属性
    struct Person{
        static var age: Int = 1
    }
    Person.age = 10
    
    

    类型属性的本质就是全局变量,在整个程序运行过程中,只有1份内存.
    我们用汇编看一下以下代码num1 , age , num2的内存地址:

    var num1 = 10
    struct Person{
        static var age: Int = 1
    }
    Person.age = 11
    var num2 = 12
    
    
    

    汇编如下:

    连续的内存地址

    会发现num1 , age , num2三个变量的地址都是连续的,说明他们都在全局区.

    类型属性还有个很重要的特性:类型属性默认是 lazy , 在第一次使用的时候才会初始化, 并且是线程安全的,只会初始化一次.

    前面我们讲延迟存储属性 lazy 关键字时说过,lazy不是线程安全的.为什么类型属性默认是lazy,它为什么是线程安全的呢?

    因为它的内部会调用swift_once``dispatch_once_f

    下面我们通过汇编证明一下,首先断点打到类型属性初始化的部分,看看类型属性初始化的函数地址.

    断点位置

    然后运行程序,看到类型属性初始化函数地址为:

    类型属性初始化函数地址

    接着把断点调整到如图所示位置:

    运行代码,分析汇编如下:

    第一步

    进入函数:

    第二步

    进入swift_once :

    swift_once 函数内部

    会发现swift_once内部会调用dispatch_once_f.

    所以现在就能明白为什么类型属性是线程安全的了,因为它的初始化代码放到dispatch_once_f中调用的.

    相关文章

      网友评论

          本文标题:从零学习Swift 07:属性

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