Swift学习(九):属性

属性分类

Swift中跟实例相关的属性可以分为2大类:

  • 存储属性(Stored Property)
  1.  类似于成员变量这个概念
  2.  存储在实例的内存中
  3.  结构体、类可以定义存储属性 
  4. 枚举不可以定义存储属性
  • 计算属性(Computed Property)
  1. 本质就是方法(函数)
  2. 不占用实例的内存
  3. 枚举、结构体、类都可以定义计算属性

由反汇编我们可以看出,只有radius这个存储属性会存储在实力对象,占用堆空间,而diameter这个计算属性相当于方法,不会占用堆空间,所以类占用的内存空间为8字节,只是radius这个存储属性的8字节


存储属性

关于存储属性,Swift有个明确的规定 :在创建类 或 结构体的实例时,必须为所有的存储属性设置一个合适的初始值

  1. 可以在初始化器里为存储属性设置一个初始值
struct Point {
    var x : Int
    var y : Int
    init () {
      x = 11
      y = 22
  }
}

     2.    可以分配一个默认的属性值作为属性定义的一部分

class Point {
    var x : Int = 11
    var y : Int = 22
}

计算属性

set传入的新值默认叫做newValue,也可以自定义

 只读计算属性:只有get,没有set

 ==> 也可以直接写为:

定义计算属性只能用var,不能用let ,因为:

  1. let代表常量:值是一成不变的 
  2. 计算属性的值是可能发生变化的(即使是只读计算属性)

注:但是计算属性不能只有set,没有get


枚举rawValue原理

枚举原始值rawValue的本质是:只读计算属性

通过反汇编,如下图:

我们可以知道,rawValue只是调用了getter方法,在上面的代码中重写了rawValue的getter方法,所以结果产生了变化,由3变为12,如图:


延迟存储属性(Lazy Stored Property)

使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化

从下面的输出结果我们可以看出,Person初始化的时候并不会初始化Car,只有在调用Car的时候才会初始化Car,这就是延迟存储属性

为什么要使用延迟存储属性? 因为有些比如数据下载等的耗时操作没必要初始化就加载,用的时候加载就好,比如下图:


延迟存储属性注意点

当结构体包含一个延迟存储属性时,只有var才能访问延迟存储属性,

因为延迟属性初始化时需要改变结构体的内存

在上面的代码中,当调用let修饰的实例对象p的延迟存储属性z时是不被允许的,因为此时会初始化z并修改p的内存,这是let修饰的常量禁止的,但是你可以调用x和y,因为他们在p初始化的时候已经初始化了,只有把p修改成var类型的,才允许调用z。


属性观察器(Property Observer)

可以为非lazy的var存储属性设置属性观察器


  在给radius赋值的时候,会先触发willset代表即将赋值,赋完值后会触发didSet,代表赋完值

  • willSet会传递新值,默认叫newValue
  • didSet会传递旧值,默认叫oldValue
  • 在初始化器中设置属性值不会触发willSet和didSet  ,即调用Circle(),触发init()方法时
  • 在属性定义时设置初始值也不会触发willSet和didSet

全局变量    局部变量

属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量身上

  num是全局变量,使用了计算属性

   age在函数中,是局部变量,使用了属性观察期


inout的本质总结

   ==> 输出结果

test(&s.width): 直接将width属性的地址值传了进去

test(&s.girth): 通过反汇编,我们可以看到先走girth的getter方法,获取到girth的值放到一个局部变量中,作为临时存储空间(栈空间),再将局部变量的地址值传入test函数,函数内部通过地址值找到存储空间,将20这个值放进去,接下来调用setter方法,将局部变量的20赋给girth的newValue,完成赋值。汇编下图:

test(&s.side): 带有观察器的存储属性赋值过程:现将side的值放到局部变量,将局部变量的地址传入test方法,完成赋值,最后通过setter方法完成赋值。

带有观察器的存储属性赋值为什么不和普通的存储属性一样直接传入地址值进行修改? 这是因为直接传入地址值进行修改不会触发willSet和didSet方法,走一个复杂的赋值过程是为了触发属性观察器。

总结:

  • 如果实参有物理内存地址,且没有设置属性观察器 : 直接将实参的内存地址传入函数(实参进行引用传递)
  • 如果实参是计算属性 或者 设置了属性观察器
  1. 采取了Copy In Copy Out的做法
  2.  调用该函数时,先复制实参的值,产生副本【get】
  3.  将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值 
  4. 函数返回后,再将副本的值覆盖实参的值【set】

总结:inout的本质就是引用传递(地址传递)


类型属性

严格来说,属性可以分为

  • 实例属性(Instance Property):只能通过实例去访问
  1. 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有1份 
  2. 计算实例属性(Computed Instance Property):带set和get方法的
  • 类型属性(Type Property):只能通过类型去访问,但是并不占用类的内存,它与全局变量类似,存储区域在全局区
  1. 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量) 
  2. 想要证明存储类型属性只占用一份内存,并且线程安全,就要通过反汇编来查看:

     1)通过反汇编,我们可以看到代码的底层调用首先来到了swift_once方法

     2)接下来继续往下走,我们可以看到它走到了核心的dispatch_once_f方法,也就是GCD申请单例的方法,这样类型属性就实现了只申请一块内存,并保证线程安全

    3.   计算类型属性(Computed Type Property)

  • 可以通过static定义类型属性 ,如果是类,也可以用关键字class,但是结构体(struct)只能使用static,不能用class

  我们看到结果为3,因为c1, c2, c3这三个实例对象中的count属性共用一块内存,所以三次调用时累加的,所以为3.


类型属性细节

  1. 不同于存储实例属性,你可以不给存储类型属性设定初始值 :因为类型没有像实例那样的init初始化器来初始化存储属性
  2. 存储类型属性默认就是lazy,会在第一次使用的时候才初始化 
  3. 就算被多个线程同时访问,保证只会初始化一次
  4. 存储类型属性可以是let
  5. 枚举类型也可以定义类型属性(存储类型属性、计算类型属性):因为存储类型属性不同于存储实例属性,不在实例对象中占用内存,而枚举中正是因为不能开辟空间存储实例属性所以不能包含存储实例属性,但是由于存储类型属性不占用实例对象内存空间的特性,所以枚举类型可以定义类型属性。

单例模式

第一种创建方式:不需要添加其他处理操作时使用,直接创建一个单例

第二种创建方式:需要添加其他处理操作时使用,用闭包的方式创建一个单例,把要添加的其他操作放在闭包中

 

 


何时用存储属性和计算属性

当你的属性想要存储下来的时候使用存储属性

当你的属性可以通过别的属性通过一定的计算方法得到,就用计算属性,否则我们还需要加个方法去表示这个属性和其他属性的关系。

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章