属性就是将值和特定的类、结构和枚举进行关联。存储属性存储常量(变量)来作为实例的一部分,而计算属性不是存储一个值。计算属性可以用于类、结构体和枚举,但是存储属性只能用于类和结构体。
存储属性和计算属性通常和特定的实例类型进行关联。但是,属性也可以作用与类型背身,这样的属性成为类型属性。
另外也可以通过定义属性观察期来监控属性值的改变,从而进行一个自定义的操作。属性观察器可以添加到自己定义的存储属性上,也可以添加到负累继承的属性上。
存储属性(Stored Properties)
简单来说,存储属性就是用来存储特定类或结构体实例中的一个常量(变量)。
可以在定义存储属性的时候给定默认值,也可以在构造的过程中设置或者修改存储属性的值,甚至是修改常量存储属性的值。
常量结构体实例的存储属性(Stored Properties of Constant Structure Instances)
如果创建了一个结构体的实例并赋值给了一个常量,即使有属性被声名为变量也无法修改该实例的任何属性:
这是因为结构体是值类型,当值类型的实例被声名为常量时,它的所有属性也都成了常量。
所以如果一个引用类型的实例赋给一个常量后,还是可以修改它的变量属性,就像类(class)。
延迟存储属性(Lazy Stored Properties)
延迟存储属性指当第一次被调用的时候才会计算初始值的属性,在属性声名前使用 lazy 关键字来标明一个延迟储存属性。
必须将延迟存储属性声名为变量(var),因为属性的初始值可能在实例构造完之后才会创建。但是常量属性在构造过程中必须要有初始值,所以没有办法声名成延迟属性。
延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它:
注意
如果一个被 lazy 标记的属性,在没有初始化的时候被多个线程访问,这种情况下不会保证只被创建一次。
存储属性和实例变量(Stored Properties and Instance Variables)
OC中为类实例存储值和引用提供了两种方法,除了实例之外还可以使用实例变量作为属性值的后端存储。
Swift中将这些概念都统一到属性中。Swift的属性没有对应的实例变量,属性的后端存储也不能直接访问。这样就能避免了不同场景下访问方式的困扰,同时也可以把属性的定义简化为一个语句。属性的全部信息——包括命名、类型和内存管理特性——都在唯一一个地方(类型定义中)定义。
计算属性(Computed Properties)
除了存储属性之外,类、结构体和枚举可以定义计算属性,这种属性实际上不能存储之。但是,会提供 getter 和可选的 setter 方法用来间接存储其他的属性和值:
便捷 Setter 声名(Shorthand Setter Declaration)
如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue:
只读计算属性(Read-Only Computed Properties)
只有 getter 没有 setter 的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点语法访问,但是不能设置新的值。
注意
只能用 var 来声名计算属性,因为计算属性不是固定的。用 let 只能来定义常量属性,表示初始化后再也无法修改的值。
只读计算属性可以去掉 get 关键字和括号:
属性观察器(Property Observers)
属性观察器监控和响应值的变化。每次属性的值被设置的时候都会调用属性观察器,甚至是新的值和当前值相同的时候。
可以给除了延时存储属性之外的任何存储属性添加属性观察器。也可以通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。不必为非重写的计算属性添加属性观察器,因为可以通过它的 setter 直接监控和响应值的变化。
可以给属性添加一个或全部的观察器:
- willSet 在值被存储之前被调用。
- didSet 在新的值被存储之后立即调用。
willSet 观察器会将新的值作为常量传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定,则新的值用 newValue 表示。
相似的,实现 didSet 观察器用一个常量参数表示旧的值,可以为该参数命名或者使用默认名称 oldValue 。如果在 didSet 中再次对该值赋值,那么新的值会覆盖旧的值。
注意
父类的属性在子类的构造器中被赋值时,它在父类中的 willSet 和 didSet 会被调用,然后才会调用子类的观察器。在父类初始化方法调用之前,子类给属性赋值时,观察器是不会被调用的。
注意
如果将属性用 in-out 的方式传入函数,willSet 和 didSet 也会调用。这是因为 in-out 参数是拷入拷出模式:就是在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。
全局和局部变量(Global and Local Variables)
计算属性和属性观察器所描述的属性也可以用在全局变量和局部变量中。全局变量是被定义在方法、函数、闭包或任何类型之外定义的变量。局部变量在函数、方法或闭包内部定义的变量。
前面提到的全局或局部变量都是属于存储变量,和存储属性类似,它为特定类型的值提供存储空间并允许值的读写。
除此之外,也可以在全局或局部范围定义计算变量和为存储变量定义观察器。计算变量和计算属性一样,返回一个计算结果而不是返回存储值,声名格式也完全一样。
注意
全局的常量(变量)都是延迟计算的,跟延迟存储属性类似,不同的地方在于,全局的常量(变量)不需要 lazy 修饰符。
局部的常量(变量)从不延迟计算。
类型属性(Type Properties)
实例属性就是一个特定类型的实例,每次创建一个新的实例,它都会有一套属于自己的属性值,并且每个实例之间的属性都是相互独立的。
也可以为类型本身定义属性,所以无论创建了多少个实例,这样的属性都只有唯一的一份。这样的属性就是类型属性。
类型属性就是用来共享某个类型的所有实例的数据,比如所有实例都能用一个常量(像C语言中的静态常量),或者都能用一个变量(C语言中的静态变量)
存储型类型属性可以是常量或者变量,但是计算型类型属性只能为变量。
注意
和存储实例属性不同,必须给存储型属性一个默认值。这是因为在存储型类型属性在初始化的时候并没有构造器为其赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会初始化。即使它们被多个线程同事访问,系统也保证只会对其初始化一次,并且不需要 lazy 修饰符。
类型属性语法(Type Property Syntax)
在C语言和OC中,与某个类型关联的静态常量(变量),是作为全局静态变量定义的。但是在Swift中,类型属性是作为类型的一部分写在类型最外层的括号内的,所以它的作用范围就是类型支持的范围内。
用 static 关键字来定义类型属性。在为类定义计算型类型属性时,可以改用 class 关键字来支持子类对父类的实现进行重写:
🌰中的计算型类型属性是只读的,但也可以定义可读写的计算型类型属性,跟计算型实例属性的语法相同。
查询和设置类型属性(Querying and Setting Type Properties)
和实例属性一样,类型属性也是通过点语法来访问的。但是类型属性是通过类型本身进行访问的,而不是通过实例:
下面有一个🌰,定义一个结构体来表示音量,每个声道的音量都是0-10之间的整数:
注意
在第一个检查过程中,didSet 属性观察期将 currentLevel 设置成了不同的值,但是这不会造成属性观察期被再次调用。