Swift 进阶(六)属性

1,295 阅读10分钟

属性的基本概念

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方法去计算

-w723

然后我们在往下执行,还会看到get方法的调用

-w722

所以可以用此证明计算属性只会生成gettersetter

注意:

一旦将存储属性变为计算属性,初始化构造器就会报错,只允许传入存储属性的值

因为存储属性是直接存储在结构体内存中的,如果改成计算属性则不会分配内存空间来存储

-w646

-w525

如果只有setter也会报错

-w651

枚举的计算属性

枚举原始值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)

-w717

可以看到底层确实是调用了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才能访问延迟存储属性

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

-w652

属性观察器(Property Observer)

可以为非lazyvar属性设置属性观察器

只有存储属性可以设置属性观察器

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

在初始化器中设置属性值不会触发willSetdidSet

struct Circle {
    // 存储属性
    var radius: Double {
        willSet {
            print("willSet", newValue)
        }
        
        didSet {
            print("didSet", oldValue, radius)
        }
    }

    init() {
        radius = 1.0
        print("Circle init!")
    }
}

var circle = Circle()

在属性定义时设置初始值也不会触发willSetdidSet

struct Circle {
    // 存储属性
    var radius: Double = 1.0 {
        willSet {
            print("willSet", newValue)
        }
        
        didSet {
            print("didSet", oldValue, radius)
        }
    }
}

var circle = Circle()

计算属性设置属性观察器会报错

-w657

全局变量和局部变量

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

全局变量

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

下面我们通过反汇编来进行分析

-w963 -w963 -w965 -w807

第二段打印

现在width的值是20,side的值是4,girth的值是80

然后调用test方法,并传入side的地址值,side变成20,并且触发属性观察器,执行打印

然后调用show方法,会调用girth的getter,然后先执行打印,再计算,girth为400

下面我们通过反汇编来进行分析

-w960 -w351

将地址值存储到rdi中,并带入到test函数中进行计算

-w959 -w960 -w870

setter中才会真正的调用willSetdidSet方法

willSetdidSet之间的计算才是真正的将改变了的值覆盖了全局变量里的side

真正改变了side的值的时候是调用完test函数之后,在内部的setter里进行的

第三段打印

现在width的值是20,side的值是20,girth的值是400

然后调用test方法,并传入girth的getter的返回值为400,然后将20赋值给girth的setter计算,width变为1

然后调用show方法,,会调用girth的getter,然后先执行打印,再计算,girth为20

下面我们通过反汇编来进行分析

-w962 -w371

-w961 -w963 -w425

-w958 -w399

-w961 -w675

-w963 -w614

-w960 -w822

-w961 -w958 -w837

再后面都是计算的过程了,这里就不详细跟进了

我们主要了解inout是怎么给计算属性进行关联调用的,从上面分析可以看出从调用girth的getter开始,都会将计算的结果放入一个寄存器中,然后通过这个寄存器的地址再进行传递,inout影响的也是修改这个寄存器中存储的值,然后再进一步传递到setter里进行计算

inout的本质总结

-w947

对于没有属性观察器的存储属性来说,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修饰存储类型属性

-w642

不同于存储实例属性,存储类型属性必须设定初始值,不然会报错

因为类型没有像实例那样的init初始化器来初始化存储属性

-w640

存储类型属性可以用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

然后我们进行反汇编来观察

-w1086 -w1086 -w1085

-w508 -w1086

通过调用我们可以发现最后会调用到GCDdispatch_once,所以存储类型属性才会说是线程安全的,并且只执行一次

并且dispatch_once里面执行的代码就是static var count = 1

单例模式

public class FileManager {
    public static let shared = FileManager()
    private init() { }
    
    public func openFile() {
        
    }
}

FileManager.shared.openFile()