Swift复习系列:属性
先来看一下百度百科关于属性的解释
属性就是对于一个对象的抽象刻画
一个具体事物,总是有许许多多的性质与关系,我们把一个事物的性质与关系,都叫做事物的性。
事物与属性是不可分的,事物都是有属性的事物,属性也都是事物的属性。
一个事物与另一个事物的相同或相异,也就是一个事物的属性与另一事物的属性的相同或相异。
由于事物属性的相同或相异,客观世界中就形成了许多不同的事物类。具有相同属性的事物就形成一类,具有不同属性的事物就分别地形成不同的类。
在Swift
中,属性将值和特定的类、结构体或者枚举关联起来。它们分为三种:存储属性、计算属性和类型属性。
- 存储属性存储常量或变量作为实例的一部分,用于类或结构体。
- 计算属性用来计算一个值,可用于类、结构体或枚举。
- 类型属性直接作用于类型本身。
存储属性
存储属性就是存储在特定类或结构体实例里面的一个常量或者变量,存储属性可以使常量存储属性也可以是变量存储属性,分别用let
或var
来定义。我们可以在定义存储属性的时候指定默认的值,也可以在构造过程中设置或修改存储属性的值,同时也可以修改常量存储属性的值(这里需要注意)。
struct ValueRange
{
var startValue:Int//变量存储属性
let length:Int//常量存储属性
}
//存储属性
var testStoreProperty = ValueRange.init(startValue: 128, length: 128)
print("-01-结构体ValueRange=\(testStoreProperty)")
//-01-结构体ValueRange=ValueRange(startValue: 128, length: 128)
//修改它的变量存储属性值
testStoreProperty.startValue = 1024
print("-02-结构体ValueRange=\(testStoreProperty)")
//-02-结构体ValueRange=ValueRange(startValue: 1024, length: 128)
在上边,我们声明了一个ValueRange
的结构体,里面包含startValue
的变量存储属性和length
的常量存储属性。然后我们用var来实例化一个结构体对象,在赋值后,我们还可以通过.
语法来访问startValue
的变量存储属性,并修改值,但是却不能修改length
的常量存储属性的值。
但是,如果我们用let来实例化ValueRange
,那又会有什么情况呢?
let testLetStoreProperty = ValueRange.init(startValue: 128, length: 256)
testLetStoreProperty.startValue = 768
如上,虽然startValue是变量存储属性,但还是会报错为:Cannot assign to property: 'testLetStoreProperty' is a 'let' constant
。这是因为Struct结构体是值类型的,当值类型被声明为常量类型时,它下面额所有属性则都变成了常量类型。
但是类(class
)却不一样,类属于引用类型,当被实例化为一个常量时,我们仍可以修改它下面的值。
存储属性的延迟存储属性
和Objective-C
中的懒加载
很相似,它在第一次被调用的时候才会初始化其属性值,我们可以在属性声明的前面使用lazy
关键字来表示延时存储属性,但是,属性声明必须使用var
来声明为变量存储属性,这是因为属性的初始值可能在构造完成之后才会被获得,而常量属性必须在构造完成之前必须要有初始值。如下
class Test01
{
var testFileName = "MyTestFileName"
}
class Test02
{
lazy var test01 = Test01()
var myTestString = String.init()
}
let test02:Test02 = Test02()
test02.myTestString += "hahahahahahahahahahahhaha"
test02.test01.testFileName += ".docx"//这个时候才开始获取Test01的实例化对象test01的初始值
print("\(test02.test01.testFileName)\n\(test02.myTestString)")
如上,Test01
的实例化对象test01
直到test02.test01.testFileName += ".docx"
时才获取到初始值。
但是,一个延时存储的属性在没有初始化时就被多个线程同时访问,我们则无法确保该属性是否会被只初始化一次。
在Swift
中的属性没有对应的实例变量,属性的后端存储也无法 直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息包括命名、类型和内存管理特征都在唯一一个地方(类型定义中)定义。
计算属性
除了存储属性之外,类、结构体和枚举也可以定义计算属性,计算属性不直接存储值,它通过一个getter
和可选的setter
来简介获取和设置其它变量或者属性的值。如下代码,
struct Point
{
var x = 0.0
var y = 0.0
}
struct Size
{
var Width = 0.0
var Height = 0.0
}
struct Rect
{
var origin = Point()
var size = Size()
var center:Point
{
get
{
let centerX = origin.x + (size.Width/2)
let centerY = origin.y + (size.Height/2)
return Point(x:centerX,y:centerY)
}
set(newPoint)
{
origin.x = newPoint.x - (size.Width/2)
origin.y = newPoint.y - (size.Height/2)
}
}
}
var square = Rect.init(origin: Point.init(x: 0, y: 0), size: Size.init(Width: 100, Height: 100))
let squareCenter = square.center
square.center = Point.init(x: 15, y: 15)
print("SquareOrigin = \(square.origin),SquareCenter = \(squareCenter)")
//SquareOrigin = Point(x: -35.0, y: -35.0),SquareCenter = Point(x: 50.0, y: 50.0)
Rect
提供了一个名为center
的计算属性。一个矩形的中心店可以从原点origin
和大小size
计算出,所以不需要像Size
和Point
那样显示声明来保存。
计算属性center
提供了自定义的getter
和setter
来获取和设置矩形的中心点和原点。
- 在上面
square
为Rect
的实例,它的center
属性可以通过.
语法squqre.center
来访问,这调用它的getter并通过计算来返回一个新的Point来表示square的中心。 - 当然我们可以直接设置
square.center
的值,这会调用它的setter
来修改origin
的值。
简化setter
声明
如果计算属性square
的setter
没有定义如上的newPoint
来表示新值的参数名,则可以使用newValue
来作为默认名称。如下
struct Rect
{
var origin = Point()
var size = Size()
var center:Point
{
get
{
let centerX = origin.x + (size.Width/2)
let centerY = origin.y + (size.Height/2)
return Point(x:centerX,y:centerY)
}
set
{
origin.x = newValue.x - (size.Width/2)
origin.y = newValue.y - (size.Height/2)
}
}
}
只读计算属性
只有getter
没有setter
的计算属性就是只读计算属性。只读计算属性可以通过.
语法来获取需要的值,但不能设置新的值。
定义计算属性,必须使用var
关键字,因为它们的值都不是固定的,同时我们还可以去掉get
关键字,如下
struct SquareRect
{
var width = 0.0
var height = 0.0
var area:Double
{
return (width * height)
}
}
属性观察器
属性观察器的作用:
监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前的值相同也会执行该操作。
我们可以为除延迟属性之外的其它存储属性
添加属性观察器,也可以通过重写属性的方式为继承的属性(这时为存储属性
和计算属性
)添加属性属性观察器。我们不必为非重写的计算属性添加属性观察器,因为通过它的setter可以直接监控和响应值的变化。
我们可以为属性添加下面的一个或全部的观察器:
-
willSet
在新的值被设置前调用 -
didSet
在新的值被设置之后立即调用 -
willSet
观察器将新的属性值作为常量参数传入,我们可以为这个参数指定一个名称,也可以使用默认的名称newValue
-
didSet
观察器会将旧的属性值作为参数传入,我们可以为该参数指定名称也可以使用oldValue
作为默认名称。在didSet
中再次对该属性值赋值,新值会覆盖旧的属性值。
还有一点需要注意:
父类的属性在子类的构造器中被赋值时,会先调用它在父类中的
willSet
和didSet
观察器,随后才会调用子类的观察器。在父类初始化方法调用之前,子类给属性赋值时,观察期不会被调用。
请看下面的例子
class BlogsCounter
{
var totalBlogs:Int = 0{
willSet(newBlogs)
{
print("All blogs is \(newBlogs)")
}
didSet
{
if totalBlogs > oldValue
{
print("The increrased blogs = \(totalBlogs - oldValue)")
}
}
}
}
let blogsCounter = BlogsCounter()
blogsCounter.totalBlogs = 128
blogsCounter.totalBlogs = 256
blogsCounter.totalBlogs = 768
blogsCounter.totalBlogs = 1024
输出结果
All blogs is 128
The increrased blogs = 128
All blogs is 256
The increrased blogs = 128
All blogs is 768
The increrased blogs = 512
All blogs is 1024
The increrased blogs = 256
Program ended with exit code: 0
willSet
在上面的例子只是负责输出新的值,参数名被自定义为newBlogs
,也可以使用默认的参数名newValue
didSet
观察器在totalBlogs
的值改变后被调用,它将新值和旧值比对,然后做相应的处理。
注意
如果将属性通过
in-out
方式传入函数,和也会调用。这是因为in-out
参数采用了拷入 拷出模式:即在函数内部使用的是参数的copy
,函数结束后,又对参数重新赋值。
全局变量和局部变量
- 计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量。全局变量是在函数、方法、闭包或任何类之外定义的变量,局部变量是在函数、方法或闭包内部定义的变量。
全局变量或局部变量都属于存储型变量,跟存储值类似,它为特定的值提供存储空间,并且允许读取和写入。 - 在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样
- 全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要 标记 lazy 修饰符。局部范围的常量或变量从不延迟计算。
类型属性
关于实例属性的类型属性的理解定义
- 实例属性就是一个特定类型的属性,每创建一个实例对象,这个实例对象就拥有一套自己的属性值,实例对象之间的属性相互独立。
- 类型属性就是为类型本身定义的属性,无论创建多少个该类型的实例,这些属性都只有唯一一份。
类型属性的作用
类型属性用于某个类型所有实例共享的数据。eg.所有实例都会用到的一个常量(类似于
C
语言中的静态常量),或者所有实例都能访问的一个变量(类似C
语言中的静态变量)。
注意
- 存储型类型属性可以是变量或者常量,计算型类型属性就和实例的计算型属性一样,只能是变量。
- 和实例的存储型属性不同,必须在初始化时给存储型类型属性一个默认值。因为类型没有构造器,所以也就无法初始化的过程中使用构造器给类型属性赋值。
- 存储型类型属性在是延时初始化的,只有第一次访问到它的时候才会被初始化,即使有多个线程同时对它进行访问,系统也会保证只对它进行一次初始化,并且它也不需要lazy修饰符。
类型属性的语法以及获取和设置类型属性的值
在C
或Objective-C
中,与某个类型关联的静态常量和静态变量,是作为全局(global
)静态变量定义的。但是在Swift
中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。
我们可以使用关键字static
来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class
来支持子类对父 类的实现进行重写。
如下
struct SomeStructure
{
static var storedTypeProperty = "Test string value"
static var computedTypeProperty:Int
{
return 128
}
}
enum SomeEnumeration
{
static var storedTypeProperty = "Test string value"
static var computedTypeProperty:Int
{
return 256
}
}
class SomeMyClass
{
static var storedTypeProperty = "Test string value"
static var computedTypeProperty:Int
{
return 512
}
class var overrideableComputedTypeProperty:Int
{
return 1024
}
}
print(SomeStructure.storedTypeProperty)
print(SomeStructure.computedTypeProperty)
print(SomeEnumeration.storedTypeProperty)
print(SomeEnumeration.computedTypeProperty)
print(SomeMyClass.storedTypeProperty)
print(SomeMyClass.computedTypeProperty)
print(SomeMyClass.overrideableComputedTypeProperty)
输出结果
Test string value
128
Test string value
256
Test string value
512
1024
Program ended with exit code: 0
在上面的例子中,计算型类型属性都是只读的,当然你也可以设置为可读可写的,请参考上面的计算型属性
。
和实例属性一样,我们都是用.
语法来访问,但不同的是类型属性使用类型名而不是类型对象名来访问。
接下来请看一个详细的例子,然后我们再具体讲解一下。
struct AudioChannel
{
static let thresholdLevel = 10//声道的音量上限值
static var maxInputLevelForAllChannels = 0//所有的声道中的最大值
var currentLevel:Int = 0
{
didSet
{
//如果当前音量大于上限值,会被修正为上限值之内
if currentLevel > AudioChannel.thresholdLevel
{
currentLevel = AudioChannel.thresholdLevel
}
//将当前音量作为新的最大输入音量
if currentLevel > AudioChannel.maxInputLevelForAllChannels
{
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
var leftChannel = AudioChannel.init()//左声道对象
var rigntChannel = AudioChannel.init()//右声道对象
leftChannel.currentLevel = 9
print("maxInputLevelForAllChannels = \(AudioChannel.maxInputLevelForAllChannels)")
rigntChannel.currentLevel = 12
print("maxInputLevelForAllChannels = \(AudioChannel.maxInputLevelForAllChannels) \ncurrentLevel = \(rigntChannel.currentLevel)")
运行结果
maxInputLevelForAllChannels = 9
maxInputLevelForAllChannels = 10
currentLevel = 10
Program ended with exit code: 0
在上面的例子中,我们定义了一个结构体,使用两个存储型类型属性来表示左声道和右声道的音量,每个声道的音量范围是0-10.
在结构体AudioChannel
中
-
thresholdLevel
为存储型类型属性,表示每个声道(AudioChannel
的实例)的音量上限值,值为常量10
;maxInputLevelForAllChannels
为静态存储型类型属性,表示所有声道(AudioChannel
的实例)中音量的最大值,是一个变量,默认值为0
; -
currentLevel
为存储型实例属性,表示该声道的当前音量值;
在存储型实例属性currentLevel
中包含didSet
属性观察器来检查每次设置后的属性值,它会做如下处理
- 如果当前声道的音量值
currentLevel
大于上限,将会被修正为上限值thresholdLevel
; - 如果修正后的音量值
currentLevel
大于已存在静态类型属性maxInputLevelForAllChannels
的值,属性观察器会将修正后的音量值currentLevel
保存到maxInputLevelForAllChannels
中 - 在第一个检查过程中,
didSet
属性观察器将currentLevel
设置成了不同的值,但这不会造成属性观察器被 再次调用。