Swift5.1 - 属性

305 阅读10分钟

属性

Swift 中跟实例相关的属性可以分为2大类

  • 存储属性(Stored Property)

    • 类似于成员变量的概念
    • 存储在实例的内存中
    • 结构体、类可以定义存储属性
    • 枚举不可以定义存储属性。(枚举的内存只存储枚举成员和关联值)
  • 计算属性(Computed Property)

    • 本质就是方法(函数)
    • 不占用实例的内存
    • 枚举、结构体、类都可以定义计算属性
struct Circle {
    // 存储属性
    var radius: Double
    
    // 计算属性
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        get {
            radius * 2
        }
    }
}

// 打印所占内存
print(MemoryLayout<Circle>.stride)   // 8, 由此看出 diameter 计算属性不占用结构体的内存

var circle = Circle(radius: 12)
print(circle.radius)    // 12
print(circle.diameter)  // 24

circle.diameter = 10
print(circle.radius)    // 5


结果: 8

计算属性使用场景: 当存在逻辑关联时可以使用计算属性。参考上面的diameter,会随着radius的变化而变化。

存储属性

对于存储属性,Swift官方有一个明确的规定

  • 在创建类和结构体的实例时,必须为所有的存储属性提供一个合适的初始值。
    • 可以在初始化器内为存储属性赋值一个初始值。
    • 也可以在定义存储属性时赋值一个初始值。
struct Circle {
    // 存储属性
    var radius: Double = 10  // 方式一
    
    // 计算属性
    var diameter: Double {
        set {
            radius = newValue / 2
        }
        get {
            radius * 2
        }
    }
    
    init(radius: Double) {
        self.radius = radius  // 方式二
    }
}

方式一和方式二本质其实是一样的。 都是在init方法内进行初始化。

计算属性

set 传入的默认值名称为newValue,也可以自定义:

struct Circle {
    // 存储属性
    var radius: Double
    
    // 计算属性
    var diameter: Double {
        set(newDiameter) {   // 自定义输入参数
            radius = newDiameter / 2
        }
        
        get {
            radius * 2
        }
    }
}

定义计算属性只能使用var,不能使用let

  • let代表常量,值是一成不变的。
  • 计算属性的值随时可能发生变化,所以只能使用var。参考上面的diameter,会随着radius发生改变。

只读计算属性

只有get方法,没有set 方法

struct Circle {
    // 存储属性
    var radius: Double
    
    // 计算属性
    var diameter: Double {
        get {
            radius * 2
        }
    }
}

可以简化为:
struct Circle {
    // 存储属性
    var radius: Double
    
    // 计算属性
    var diameter: Double { radius * 2 }
}

注意: 但是不能只有set方法,没有get方法。

枚举rawValue的原理

枚举rawValue的本质: 只读计算属性

enum TestEnum: Int {
    case test1 = 1, test2 = 2, test3 = 3, test4 = 4
   
    var rawValue: Int {
        switch self {
        case .test1:
            return 10
        case .test2:
            return 2
        case .test3:
            return 3
        case .test4:
            return 4
        }
    }
}

var ts = TestEnum.test1
print(ts.rawValue)  // 10

延迟存储属性

使用lazy关键词可以定义一个延迟存储属性,在第一次用到属性的时候才会被初始化。

class Car {
    init() {
        print("Car init")
    }
    
    func run() {
        print("Car run")
    }
}

class Person {
    lazy var car = Car()
    init() {
        print("Person init")
    }
    
    func goRun() {
        car.run()
        print("Go Run")
    }
}


var person = Person()
print("--------")
person.goRun()

打印结果:
Person init
--------
Car init
Car run
Go Run
  • lazy 属性必须是var, 不能是let
    • let 必须在实例的初始化方法完成之前就拥有值。
  • 如果多条线程同时第一次访问lazy属性,不能保证属性只被初始化一次。
  • 应用场景: 如图片的加载。

延迟存储属性注意点

当结构体包含一个延迟存储属性时,只有var才能访问延迟存储属性。

  • 因为延迟属性初始化时需要修改结构体的内存。
struct Point {
    var x = 0
    var y = 0
    lazy var z = 0   // 初始化时需要修改结构体的内存
}

let p = Point()
print(p.z)  // Cannot use mutating getter on immutable value: 'p' is a 'let' constant

但是改为class 就可以,这也是struct和class的区别

class Point {
    var x = 0
    var y = 0
    lazy var z = 0   // 初始化时需要修改内存
}

let p = Point()
print(p.z)  // 正常调用,此时修改的是堆空间内存

属性观察器(Property Observer)

可以为非lazy的var存储属性设置属性观察器。

struct Circle {
    var radius: Double {
        willSet {
            print("willSet", newValue)
        }

        didSet {
            print("oldValue", oldValue)
        }
    }
    
    init() {
        self.radius = 1.0
        print("Circle init")
    }
}

var circle = Circle()
circle.radius = 10.5

print(circle.radius)
  • willSet 会传递新值,默认名称为newValue
  • didSet 会传递旧值,默认名称为oldValue
  • 在初始化器中设置属性的值,不会触发willSet和didSet
  • 在定义时给属性初始值,也不会触发willSet和didSet

全局变量、局部变量

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

var test: Int {
    get {
        print("xxxxxx  get")
        return 0
    }
    
    set {
        print("yyyyy  set")
    }
}

func test1() {
    var age: Int = 0 {
        willSet {
            print("xxxxxx  willSet", newValue)
        }
        
        didSet {
            print("xxxxxx  didSet", oldValue)
        }
    }
    
    age = 22
}

test1()

inout的再次研究

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetdid")
        }
    }
    
    var girth: Int {
        set {
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            return width * side
        }
    }
    
    func show() {
        print("width = \(width), side = \(side), girth = \(girth)")
    }
}

// 传入一个inout 参数
func test(_ num: inout Int) {
    num = 20
}

一、 当传递正常的存储属性时(没有添加属性观察器)

var s = Shape(width: 10, side: 4)
test(&s.width)
s.show()

打印:
getGirth
width = 20, side = 4, girth = 80

汇编分析

  1. 切换为汇编模式:Xcode -> Debug -> Debug Work Flow -> alway show disassembly

  2. 在调用test()方法的地方打个断点,运行程序

    avatar

  3. 可以看到,在调用test方法前, 全局变量s的地址(也是width属性的地址)先放到了%rdi(一般用于存储函数参数) 中, 即 将width属性的地址传给了test函数。

    avatar

  4. 在控制台上敲si (汇编模式下的进入调用方法的指令), 进入test函数内, 可以看到通过movq 指令将立即数$0x14(即20) 赋值给了%rdi (width属性对应的内存空间)

    avatar

结论:inout 函数本质上就是引用传递。

二、 当传递计算属性时

var s = Shape(width: 10, side: 4)
test(&s.girth)
s.show()

打印:
getGirth
setGirth 20
getGirth
width = 5, side = 4, girth = 20

发现也能修改成功。那它是如何进行赋值的呢?下面通过汇编分析一下:

avatar
根据上面汇编可以看到:

  1. 在调用test方法之前, 首先调用了girth属性的getter方法,拿到girth的值
    • 0x10000143c <+92>: callq 0x1000017e0 ; LearningSwift.Shape.girth.getter : Swift.Int at main.swift:148
  2. 拿到girth的值后,赋值给一个局部变量-0x28(%rbp), 然后又将局部变量的地址赋给%rdi (test函数的参数)
    • 0x100001441 <+97>: movq %rax, -0x28(%rbp)
    • 0x100001445 <+101>: leaq -0x28(%rbp), %rdi
  3. 调用test()函数, 传参是局部变量的地址。然后在test函数内部将20 赋值给局部变量。(和上面的(一)正常赋值一样)
  4. 调用完test函数之后,又将局部变量的值给了%rdi
    • 0x10000144e <+110>: movq -0x28(%rbp), %rdi
  5. 接着会调用了girth属性的setter方法,取出%rdi里的值,赋值给girth
    • 0x100001459 <+121>: callq 0x100001930 ; LearningSwift.Shape.girth.setter : Swift.Int at main.swift:144

总结: 计算属性传递给带有inout参数的函数(比方:test函数)时,会先调用计算属性的getter方法,拿到计算属性的值,赋值给一个局部变量(比方为:a), 然后将a 地址传递给test函数, 在test函数内部修改a的值, 当test函数调用完毕后, 再调用计算属性的setter方法,将a的值传给计算属性。

三、 当传递带有属性监听器的存储属性时

var s = Shape(width: 10, side: 4)
test(&s.girth)
s.show()

打印:
willSetSide 20
didSetdid
getGirth
width = 10, side = 20, girth = 200

有属性监听器和没有属性监听器调用test函数时有什么区别呢?下面通过汇编分析一下:

avatar

根据上面汇编可以看到:

  1. 在调用test方法之前, 首先获取side的值,赋值给一个局部变量-0x28(%rbp)。
    • 0x10000142e <+78>: movq 0x864b(%rip), %rax ; LearningSwift.s : LearningSwift.Shape + 8 // +8 代表访问的是side属性
    • 0x100001435 <+85>: movq %rax, -0x28(%rbp)
  2. 将局部变量的地址赋值给inout参数,调用test函数。
    • 0x100001439 <+89>: leaq -0x28(%rbp), %rdi
    • 0x10000143d <+93>: callq 0x100001e70 ; LearningSwift.test(inout Swift.Int) -> () at main.swift:160
  3. 调用完test之后,又将局部变量的值给了%rdi
    • 0x100001442 <+98>: movq -0x28(%rbp), %rdi
  4. 接着会调用了side的setter方法,更改girth的值, 同时会触发属性监听器,willSet 和 didSet
    • 0x10000144d <+109>: callq 0x100001540 ; LearningSwift.Shape.side.setter : Swift.Int at

总结: 带有属性监听器的存储属性调用带有inout参数的test函数,和计算属性本质是一样的,都会产生一个局部变量,然后将局部变量的地址传递给test函数, 调用完test函数后,再调用对应属性的setter方法。

inout 的本质总结

如果实参有物理内存地址,且没有设置属性观察器。

  • 直接将实参的内存地址传入函数。

如果实参是计算属性或者设置了属性观察器

  • 采取了Copy In Copy Out的做法
    • 调用该函数时,先复制实参的值,产生副本[get]
    • 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
    • 函数返回后, 再将副本的值覆盖实参的值[set]

总结: inout的本质就是引用传递。

类型属性(Type Property)

严格来说, 属性可以分为:

  • 实例属性(instance property): 只能通过实例访问
    • 存储实例属性:(stored instance property): 存储在实例的内存中,每个实例都有1份。
    • 计算实例属性:(computed instance property)
  • 类型属性(type property): 只能通过类型访问
    • 存储类型属性:(stored type property): 整个程序运行过程中,就只有一份内存。(类似于全局变量)
    • 计算类型属性:(computed type property)

可以通过static 关键词来定义类型属性, 如果是类,也可以通过class 关键词来定义。

struct Cat {
    // 存储实例属性
    var name: String
    // 计算实例属性
    var age: Int { 10 }
    
    // 存储类型属性
    static var width: Int = 10
    // 计算类型属性
    static var height: Int { 10 }
}

var cat = Cat(name: "abc")
print(cat.name)  // 实例属性只能通过实例来调用
print(cat.age)

print(Cat.width) // 类型属性只能通过类型来调用
print(Cat.height)

类型属性的细节

  • 不同于存储实例属性,你必须给存储类型属性设定初始值。
    • 因为类型属性没有像实例那样的init初始化器来初始化存储属性。
  • 存储类型属性默认就是lazy,会在第一次使用的时候才初始化。
    • 就算被多线程同时访问,保证只会被初始化一次。
    • 存储类型属性可以是let
  • 枚举类型也可以定义类型属性(存储类型属性,计算类型属性)。
    • 因为类型属性的内存并不需要存储在枚举实例内。

单例模式

public class FileManager {
    // public: 保证外部能访问到    static:保证全局只有一份内存, let: 保证share 指向不会发生变更
    public static let share = FileManager()
    
    // 私有化初始化器,禁止外部创建实例
    private init() {
        
    }
    
    func test() {
        print("FileManager test")
    }
}

// 方法调用
FileManager.share.test()

总结自 MJ Swift 教程