属性
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
汇编分析
-
切换为汇编模式:Xcode -> Debug -> Debug Work Flow -> alway show disassembly
-
在调用test()方法的地方打个断点,运行程序

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

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

结论:inout 函数本质上就是引用传递。
二、 当传递计算属性时
var s = Shape(width: 10, side: 4)
test(&s.girth)
s.show()
打印:
getGirth
setGirth 20
getGirth
width = 5, side = 4, girth = 20
发现也能修改成功。那它是如何进行赋值的呢?下面通过汇编分析一下:

- 在调用test方法之前, 首先调用了girth属性的getter方法,拿到girth的值
- 0x10000143c <+92>: callq 0x1000017e0 ; LearningSwift.Shape.girth.getter : Swift.Int at main.swift:148
- 拿到girth的值后,赋值给一个局部变量-0x28(%rbp), 然后又将局部变量的地址赋给%rdi (test函数的参数)
- 0x100001441 <+97>: movq %rax, -0x28(%rbp)
- 0x100001445 <+101>: leaq -0x28(%rbp), %rdi
- 调用test()函数, 传参是局部变量的地址。然后在test函数内部将20 赋值给局部变量。(和上面的(一)正常赋值一样)
- 调用完test函数之后,又将局部变量的值给了%rdi
- 0x10000144e <+110>: movq -0x28(%rbp), %rdi
- 接着会调用了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函数时有什么区别呢?下面通过汇编分析一下:

根据上面汇编可以看到:
- 在调用test方法之前, 首先获取side的值,赋值给一个局部变量-0x28(%rbp)。
- 0x10000142e <+78>: movq 0x864b(%rip), %rax ; LearningSwift.s : LearningSwift.Shape + 8 // +8 代表访问的是side属性
- 0x100001435 <+85>: movq %rax, -0x28(%rbp)
- 将局部变量的地址赋值给inout参数,调用test函数。
- 0x100001439 <+89>: leaq -0x28(%rbp), %rdi
- 0x10000143d <+93>: callq 0x100001e70 ; LearningSwift.test(inout Swift.Int) -> () at main.swift:160
- 调用完test之后,又将局部变量的值给了%rdi
- 0x100001442 <+98>: movq -0x28(%rbp), %rdi
- 接着会调用了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()