前言
属性将值跟特定的类、结构或枚举关联。存储属性存储常量或变量作为实例的一部分,而计算属性计算(不是存储)一个值。计算属性可以用于类、结构体和枚举,存储属性只能用于类和结构体。
存储属性和计算属性通常与特定类型的实例关联。但是,属性也可以直接作用于类型本身,这种属性称为类型属性。
另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己定义的存储属性上,也可以添加到从父类继承的属性上。
Swift
属性可以分为 实例属性和类型属性
实例属性
实例属性可以分为 存储属性 和 计算属性。
存储属性
一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字 var
定义),也可以是常量存储属性(用关键字 let
定义)。
- 类似于成员变量
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举定义存储属性
关于存储属性,Swift
中明确规定,在创建类或者结构体的实例时,必须为所有的存储属性设置一个合适的初始值:
1)可以在初始化器里为存储属性设置设置一个初始值;
2)可以分配一个默认的属性值作为属性定义的一部分。
struct Point {
var x: Int
let y: Int
}
var p1 = Point(x: 1, y: 1)
p1.x = 2
p1.y = 2 // 报错,y 是常量存储属性
let p2 = Point(x: 10, y: 10)
p2.x = 12 // 报错,结构体(Point)是值类型。当值类型的实例被声明为常量的时候,其属性也就成了常量
class Point {
var x: Int
let y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 1, y: 1)
p1.x = 2
p1.y = 2 // 报错,y 是常量存储属性
let p2 = Point(x: 10, y: 10)
p2.x = 12 // 不报错,类引用类型,把引用类型的实例赋给一个常量后,仍然可以修改该实例的变量属性。
延迟存储属性
延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy
来标示一个延迟存储属性。
实例:Person
类和 House
类,Person
类中有个存储属性 house
,在实例化 Person
对象的时候由于 house
使用了 lazy
,所以 house
只有在被第一使用的时候才会创建(调用 House
类中的相关方法)
class House {
var name: String
init(name: String) {
self.name = name
print("init \(name) House")
}
func sleep() {
print("sleep")
}
}
class Person {
lazy var house = House(name: "天桥")
init() {
print("init Person")
}
func goHome() {
house.sleep()
}
}
let p = Person()
print("House 还没被创建")
p.goHome()
输出结果:
init Person
House 还没被创建
init 天桥 House
sleep
如果不使用 lazy
则:
init 天桥 House
init Person
House 还没被创建
sleep
延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。
1.必须将延迟存储属性声明成变量(使用 var
关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性(let
)在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。
2. 如果一个被标记为 lazy
的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。
计算属性(Computed Property)
计算属性不直接存储值,不用初始化,而是提供一个 getter
和一个可选的 setter
,来间接获取和设置其他属性或变量的值。
- 本质就是方法(函数)
- 不占用实例的内存
- 枚举、结构体、类都可以定义计算属性
struct Square {
var width: Double
var are: Double {
get {
width * width // 只有一条 return 语句,省略 return 关键字
}
set(newAre){
width = sqrt(newAre)
}
/**
set 传入新值的默认默认名称为 newValue,所以可以简化如下
set {
width = sqrt(newValue)
}
*/
}
}
var s = Square(width: 3)
print("width:\(s.width), are:\(s.are)")
计算属性的 set
方法,传入的新值默认叫 newValue
,如上面实例中注释掉的 set
方法,当然也可以自定义新值的名称,如上面的 newAre
。
只读计算属性
只有 getter
没有 setter
的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。
struct Square {
var width = 0.0
var are:Double {
get {
width * width
}
}
}
var s = Square(width: 2)
print("are:\(s.are)")
只读计算属性的声明可以去掉 get
关键字和花括号,所以:
struct Square {
var width = 0.0
var are: Double {
width * width
}
}
必须使用 var
关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let
关键字只用来声明常量属性,表示初始化后再也无法修改的值。
属性观察器
属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。
可以为非 lazy
修饰的 var
的存储属性添加观察器。
class Animal {
var age: Int {
//新值存储之前调用
willSet {
print("Animal willSet newValue:\(newValue)")
}
//新值存储之后调用
didSet {
print("Animal didSet oldValue:\(oldValue), age:\(age)")
}
}
init(age: Int) {
self.age = age
}
}
var a = Animal(age: 1)
a.age = 5
Animal willSet newValue:5
Animal didSet oldValue:1, age:5
-
willSet
在新的值被设置之前调用:
willSet
观察器会将新的属性值作为常量参数传入,在willSet
的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称newValue
表示。 -
didSet
在新的值被设置之后立即调用:
didSet
观察器会将旧的属性值作为参数传入,可以为该参数命名或者使用默认参数名oldValue
。如果在didSet
方法中再次对该属性赋值,那么新值会覆盖旧的值。 - 在初始化器中设置属性值,不会出发
willSet
和didSet
。
父类和子类针对同一个存储属性同时存在属性观察器
class Dog: Animal {
override var age: Int {
willSet {
print("Dog willSet newValue:\(newValue)")
}
didSet {
print("Dog didSet oldValue:\(oldValue), age:\(age)")
}
}
}
var d = Dog(age: 6)
d.age = 10
Dog willSet newValue:10
Animal willSet newValue:10
Animal didSet oldValue:6, age:10
Dog didSet oldValue:6, age:10
对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willSet
,后父类 willSet
,再父类 didSet
, 子类的 didSet
父类的属性在子类的构造器中被赋值时,它在父类中的 willSet
和 didSet
观察器会被调用,随后才会调用子类的观察器。
番外 - 各种实例属性通过 inout
方式传入函数
存储属性通过 inout
方式传入函数
func inoutFunc(v1: inout Int) {
v1 = 666
print("inoutFunc")
}
struct Line {
var width = 1
}
var l = Line()
inoutFunc(v1: &l.width)
print("Line inout:\(l.width)") //输出 Line inout:666
带有属性观察器的存储属性通过 inout
方式传入函数
struct Line {
var width = 1 {
willSet {
print("Line willSet")
}
didSet {
print("Line didSet")
}
}
}
var l = Line()
inoutFunc(v1: &l.width)
print("Line inout:\(l.width)")
输出信息:
inoutFunc
Line willSet
Line didSet
Line inout:666
可以看到在 inoutFunc
函数打印信息之后调用了 willSet
和 didSet
计算属性通过 inout
方式传入函数
struct Line {
var width = 1
var widthTest: Int {
set {
width = newValue
print("Line set")
}
get {
print("Line get")
return width
}
}
}
var l = Line()
inoutFunc(v1: &l.widthTest)
print("Line inout:\(l.width)")
输出:
Line get
inoutFunc
Line set
Line inout:666
可以看到,传入 inoutFunc
函数时,通过 get
取到值,完成 inoutFunc
后,又调用了 set
方法,设置值。
- 如果实参有物理内存地址,且没有设置属性观察器
直接将实参的内存地址传入函数(实参进行引用传递) - 如果实参是计算属性 或者 设置了属性观察器
采取了 拷入拷出模式(在函数内部使用的是参数的copy
,函数结束后,又对参数重新赋值。)
1.调用该函数时,先复制实参的值,产生副本【get】
2.将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
3.函数返回后,再将副本的值覆盖实参的值【set】
总结: inout
的本质就是引用传递(地址传递)
全局变量和局部变量
计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。
全局或局部变量都属于存储型变量,跟存储属性类似,它为特定类型的值提供存储空间,并允许读取和写入。
另外,在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。计算型变量跟计算属性一样,返回一个计算结果而不是存储值,声明格式也完全一样。
var number: Int {
set {
print("number set")
}
get {
print("number get")
return 1
}
}
number = 11
print(number)
打印信息:
number set
number get
1
类型属性
为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份,这种属性就是类型属性。只能通过类型去访问。
类型属性可以分为:存储型类型属性和计算型类型属性。
存储型类型属性 - 可以是变量或常量,在整个程序运行过程中,只有 1 份内存(类似全局变量)。
计算型类型属性 - 跟实例的计算型属性一样只能定义成变量属性。
- 跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
- 存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用
lazy
修饰符。 - 存储型类型属性在初始化的时候,通过断点调试可以看到起调用 swift_once 并最终调用 dispatch_once 对存储型类型属性进行初始化(赋初值)
使用关键字 static
来定义类型属性。如果是类的计算型类型属性,可以改用关键字 class
。
枚举、结构体和类均可以定义类型属性:
enum PointEnum {
static var x: Int = 0
static var y: Int {
return 0
}
}
struct PointStuct {
static var x: Int = 1
static var y: Int {
return 1
}
}
class PointClass {
static var x: Int = 2
static var y: Int {
return 2
}
class var z: Int {
return 2
}
}
网友评论