属性的基本概念
Swift中跟实例相关的属性可以分为2大类
- 存储属性(Stored Property)
- 类似于成员变量的概念
- 存储在实例的内存中
- 结构体、类可以定义存储属性
- 枚举不可以定义存储属性
- 计算属性(Computed Property)
- 本质就是方法(函数)
- 不占用实例的内存
- 枚举、结构体、类都可以定义计算属性
存储属性
关于存储属性,Swift有个明确的规定:在创建类或结构体的实例时,必须为所有的存储属性设置一个合适的初始值
可以在初始化器里为存储属性设置一个初始值
struct Point {
// 存储属性
var x: Int
var y: Int
}
let p = Point(x: 10, y: 10)
可以分配一个默认的属性值作为属性定义的一部分
struct Point {
// 存储属性
var x: Int = 10
var y: Int = 10
}
let p = Point()
计算属性
定义计算属性只能用var
,不能用let
let
代表常量,值是一直不变的- 计算属性的值是可能发生变化的(即使是只读计算属性)
struct Circle {
// 存储属性
var radius: Double
// 计算属性
var diameter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
var circle = Circle(radius: 5)
circle.diameter = 12
print(circle.diameter)
set传入的新值默认叫做newValue
,也可以自定义
struct Circle {
// 存储属性
var radius: Double
// 计算属性
var diameter: Double {
set(newDiameter) {
radius = newDiameter / 2
}
get {
radius * 2
}
}
}
var circle = Circle(radius: 5)
circle.diameter = 12
print(circle.diameter)
只读计算属性,只有get
,没有set
struct Circle {
// 存储属性
var radius: Double
// 计算属性
var diameter: Double {
get {
radius * 2
}
}
}
struct Circle {
// 存储属性
var radius: Double
// 计算属性
var diameter: Double { radius * 2 }
}
}
打印Circle结构体
的内存大小,其占用才8个字节
,其本质是因为计算属性相当于函数
var circle = Circle(radius: 5)
print(Mems.size(ofVal: &circle)) // 8
我们可以通过反汇编来查看其内部做了什么
可以看到内部会调用set方法
去计算
然后我们在往下执行,还会看到get方法
的调用
所以可以用此证明计算属性只会生成getter
和setter
注意:
一旦将存储属性变为计算属性,初始化构造器就会报错,只允许传入存储属性的值
因为存储属性是直接存储在结构体内存中的,如果改成计算属性则不会分配内存空间来存储
如果只有setter
也会报错
枚举的计算属性
枚举原始值rawValue
的本质也是计算属性,而且是只读的计算属性
enum TestEnum: Int {
case test1, test2, test3
var rawValue: Int {
switch self {
case .test1:
return 10
case .test2:
return 20
case .test3:
return 30
}
}
}
print(TestEnum.test1.rawValue)
下面我们去掉自己写的rawValue
,然后转汇编看下本质是什么样的
enum TestEnum: Int {
case test1, test2, test3
}
print(TestEnum.test1.rawValue)
可以看到底层确实是调用了getter
延迟存储属性(Lazy Stored Property)
使用lazy
可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
看下面的示例代码,如果不加lazy
,那么Person初始化之后就会进行Car的初始化
加上lazy
,只有调用到属性的时候才会进行Car的初始化
class Car {
init() {
print("Car init!")
}
func run() {
print("Car is running!")
}
}
class Person {
lazy var car = Car()
init() {
print("Person init!")
}
func goOut() {
car.run()
}
}
let p = Person()
print("----")
p.goOut()
// 打印:
// Person init!
// ----
// Car init!
// Car is running!
lazy
属性必须是var
,不能是let
let
必须在实例的初始化方法完成之前就拥有值
class PhotoView {
lazy var image: UIImage = {
let url = "http://www.***.com/logo.png"
let data = Data(url: url)
return UIImage(data: data)
}()
}
注意:lazy
属性和普通的存储属性内存布局是一样的,不同的只是什么时候会被放进内存中而且
延迟存储属性的注意点
1.如果多条线程同时第一次访问lazy
属性,无法保证属性只被初始化一次
2.当结构体包含一个延迟存储属性时,只有var
才能访问延迟存储属性
因为延迟存储属性初始化时需要改变结构体的内存
属性观察器(Property Observer)
可以为非lazy
的var属性
设置属性观察器
只有存储属性可以设置属性观察器
willSet
会传递新值,默认叫newValue
didSet
会传递旧值,默认叫oldValue
struct Circle {
// 存储属性
var radius: Double {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
init() {
radius = 1.0
print("Circle init!")
}
}
var circle = Circle()
circle.radius = 10.5
// 打印
// willSet 10.5
// didSet 1.0 10.5
在初始化器中设置属性值不会触发willSet
和didSet
struct Circle {
// 存储属性
var radius: Double {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
init() {
radius = 1.0
print("Circle init!")
}
}
var circle = Circle()
在属性定义时设置初始值也不会触发willSet
和didSet
struct Circle {
// 存储属性
var radius: Double = 1.0 {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
}
var circle = Circle()
计算属性设置属性观察器会报错
全局变量和局部变量
属性观察器、计算属性的功能,同样可以应用在全局变量和局部变量身上
全局变量
var num: Int {
get {
return 10
}
set {
print("setNum", newValue)
}
}
num = 11 // setNum 11
print(num) // 10
局部变量
func test() {
var age = 10 {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, age)
}
}
age = 11
// willSet 11
// didSet 10 11
}
test()
inout对属性的影响
看下面的示例代码,分别输出什么,为什么?
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, side)
}
}
var girth: Int {
set {
width = newValue / side
print("setWidth", newValue)
}
get {
print("getWidth")
return width * side
}
}
func show() {
print("width=\(width), side=\(side), girth=\(girth)")
}
}
func test(_ num: inout Int) {
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.width)
s.show()
print("--------------------")
test(&s.side)
s.show()
print("--------------------")
test(&s.girth)
s.show()
// 打印:
// getWidth
// width=20, side=4, girth=80
// --------------------
// willSet 20
// didSet 4 20
// getWidth
// width=20, side=20, girth=400
// --------------------
// getWidth
// setWidth 20
// getWidth
// width=1, side=20, girth=20
第一段打印
初始化的时候会给width赋值为10,side赋值为4,并且不会调用side的属性观察器
然后调用test方法
,并传入width的地址值,width变成20
然后调用show方法
,会调用girth的getter,然后先执行打印,再计算,girth为80
下面我们通过反汇编来进行分析
第二段打印
现在width的值是20,side的值是4,girth的值是80
然后调用test方法
,并传入side的地址值,side变成20,并且触发属性观察器,执行打印
然后调用show方法
,会调用girth的getter,然后先执行打印,再计算,girth为400
下面我们通过反汇编来进行分析
将地址值存储到rdi中,并带入到test函数中进行计算
setter
中才会真正的调用willSet
和didSet
方法
willSet
和didSet
之间的计算才是真正的将改变了的值覆盖了全局变量里的side
真正改变了side的值的时候是调用完test函数
之后,在内部的setter
里进行的
第三段打印
现在width的值是20,side的值是20,girth的值是400
然后调用test方法
,并传入girth的getter的返回值为400,然后将20赋值给girth的setter计算,width变为1
然后调用show方法
,,会调用girth的getter,然后先执行打印,再计算,girth为20
下面我们通过反汇编来进行分析
再后面都是计算的过程了,这里就不详细跟进了
我们主要了解inout
是怎么给计算属性进行关联调用的,从上面分析可以看出从调用girth的getter
开始,都会将计算的结果放入一个寄存器中,然后通过这个寄存器的地址再进行传递,inout
影响的也是修改这个寄存器中存储的值,然后再进一步传递到setter
里进行计算
inout的本质总结
对于没有属性观察器的存储属性来说,inout
的本质就是传进来一个地址值,然后将值存储到这个地址对应的存储空间内
对于设置了属性观察器和计算属性来说,inout
会先将传进来的地址值放到一个局部变量中,然后改变局部变量地址值对应的存储空间
再将改变了的值覆盖最初传进来的参数的值,这时会对应触发属性观察器willSet、didSet
和计算属性的setter、getter
的调用
如果不这么做,直接就改变了传进来的地址值的存储空间的话,就不会调用属性观察器了,而计算属性因为没有分配内存来存储值,也就没办法更改了
inout
的本质就是引用传递(地址传递)
类型属性(Type Property)
严格来说,属性可以分为两大类
- 实例属性(Instance Property):只能通过实例去访问
- 存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有一份
- 计算实例属性(Computed Instance Property)
- 类型属性(Type Property):只能通过类去访问
- 存储类型属性(Stored Type Property):整个程序运行过程中,就只有一份内存(类似于全局变量)
- 计算类型属性(Computed Type Property)
可以通过static
定义类型属性
struct Car {
static var count: Int = 0
init() {
Car.count += 1
}
}
如果是类,也可以用关键字class
修饰计算类型属性
class Car {
class var count: Int {
return 10
}
}
print(Car.count)
类里面不能用class
修饰存储类型属性
不同于存储实例属性,存储类型属性必须设定初始值,不然会报错
因为类型没有像实例那样的init初始化器
来初始化存储属性
存储类型属性可以用let
struct Car {
static let count: Int = 0
}
print(Car.count)
枚举类型也可以定义类型属性(存储类型属性、计算类型属性)
enum Shape {
static var width: Int = 0
case s1, s2, s3, s4
}
var s = Shape.s1
Shape.width = 5
存储类型属性默认就是lazy,会在第一次使用的时候进行初始化
就算被多个线程同时访问,保证只会初始化一次
通过反汇编来分析类型属性的底层实现
我们先通过打印下面两组代码来做对比,发现存储类型属性的内存地址和前后两个全局变量正好相差8个字节,所以可以证明存储类型属性的本质就是类似于全局变量,只是放在了结构体或者类里面控制了访问权限
var num1 = 5
var num2 = 6
var num3 = 7
print(Mems.ptr(ofVal: &num1)) // 0x000000010000c1c0
print(Mems.ptr(ofVal: &num2)) // 0x000000010000c1c8
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c1d0
var num1 = 5
class Car {
static var count = 1
}
Car.count = 6
var num3 = 7
print(Mems.ptr(ofVal: &num1)) // 0x000000010000c2f8
print(Mems.ptr(ofVal: &Car.count)) // 0x000000010000c300
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c308
然后我们进行反汇编来观察
通过调用我们可以发现最后会调用到GCD
的dispatch_once
,所以存储类型属性才会说是线程安全的,并且只执行一次
并且dispatch_once
里面执行的代码就是static var count = 1
单例模式
public class FileManager {
public static let shared = FileManager()
private init() { }
public func openFile() {
}
}
FileManager.shared.openFile()