Swift中跟实例相关的属性可以分为2大类,计算属性和存储属性
struct Circle {
// 存储属性
var radius: Double
// 计算属性
var diameter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
存储属性(Stored Property)
类似于成员变量的概念
存储在实例的内存中
结构体和类可以定义存储属性
枚举不可以定义存储属性
计算属性(Computed Property)
本质就是方法(函数)
不占用实例的内存
枚举、结构体、类都可以定义计算属性
存储属性
关于存储属性,Swift有个明确的规定
在创建类 或 结构体的实例时,必须为所有的存储属性设置一个合适的初始值
- 可以在初始化器里为存储属性设置一个初始值 ```swift struct Point { var x: Int var y: Int }
var p = Point(x: 10, y: 20)
- 可以分配一个默认的属性值作为属性定义的一部分
```swift
struct Point {
var x: Int = 10
var y: Int = 20
}
var p = Point()
计算属性
set传入的新值默认叫做newValue,也可以自定义:
struct Point {
var x: Int = 10
var y: Int {
set(myNewValue) {
x = myNewValue * 2
}
get {
return x/2
}
}
}
只读计算属性:只有get,没有set
struct Point {
var x: Int = 10
var y: Int {
get {
20
}
}
}
计算属性只能用var,不能用let
(因为存储的内容是会变的)
枚举rawValue原理
enum Season: Int {
case spring = 1, summer, automn, winter
var rawValue: Int {
switch self {
case .spring:
return 11
case .summer:
return 22
case .automn:
return 33
case .winter:
return 44
}
}
}
var s = Season.summer
print(s.rawValue) // 22
rawValue本质是只读的计算属性
可以通过汇编查看:
0x1000039dd <+93>: callq 0x100003af0 ; SwiftTest.Season.rawValue.getter : Swift.Int at <compiler-generated>
延迟存储属性(Lazy Stored Property)
使用lazy可以定义一个延迟存储属性,在第一次用到属性的时候才会进行初始化
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()
}
}
var p = Person()
print("-----------")
p.goOut()
打印结果:
Person init!
-----------
Car init!
Car is running!
创建Person的过程中,没有创建car属性,在用到car属性时才会调用car的初始化器,进行创建。
lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "123.png")
return imageView
}()
imageView是一个存储属性,只不过是一个带有返回值的闭包表达式,用lazy修饰后,在使用时,就会执行闭包表达式里的内容。
延迟存储属性特点
lazy属性必须是var,不能是let,因为let必须在实例的初始化方法完成之前拥有值。
如果多条线程同时第一次访问lazy属性,无法保证属性只被初始化一次。
延迟存储属性注意点
当结构体包含一个延迟存储属性是,只有var才能访问延迟存储属性,因为延迟存储属性初始化时需要改变结构体的内存
属性观察器(Property Observer)
可以为非lazy的var存储属性设置属性观察器
struct Point {
var x: Int = 10
var y: Int = 20 {
willSet {
print("willset - newValue = \(newValue), y = \(y)")
}
didSet {
print("didset - oldValue = \(oldValue), y = \(y)")
}
}
}
var p = Point()
p.y = 21
打印结果:
willset - newValue = 21, y = 20
didset - oldValue = 20, y = 21
willSet会传递新值,默认叫newValue
didSet会传递旧值,默认叫oldValue
在初始化器中设置属性值不会出发willset和didSet
在属性定义时设置初始值也不会出发willSet和didSet
全局变量、局部变量
属性观察器、计算属性的功能,同样可以应用在全局变量、局部变量身上
/// 全局变量
var num: Int {
set {
print("setNum", newValue)
}
get {
10
}
}
/// 局部变量
func test() {
var age: Int = 10 {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, age)
}
}
age = 11
print("age = \(age)")
}
test()
inout的再次研究
struct Point {
/// 存储属性
var x: Int
/// 计算属性
var y: Int {
set {
x = newValue / 2
}
get {
x * 2
}
}
func log() {
print("point = (\(x), \(y))")
}
}
/// inout参数
func test(_ num: inout Int) {
num = 20
}
// 创建point实例
var p = Point(x: 1)
p.log()
// 传入p的计算属性作为inout参数
test(&p.y)
p.log()
打印结果:
point = (1, 2)
point = (10, 20)
证明计算属性也可以作为inout参数,原理是调用test方法前,会先调用y的get方法拿到y的值并创建一个局部变量,再调用test方法把这个局部变量的地址值进行赋值,最后调用y的set方法将局部变量赋值给y。可以通过汇编验证:
0x100003c49 <+57>: callq 0x100003cc0 ; SwiftTest.Point.y.getter : Swift.Int at main.swift:440
0x100003c4e <+62>: movq %rax, -0x28(%rbp)
0x100003c52 <+66>: leaq -0x28(%rbp), %rdi
0x100003c56 <+70>: callq 0x100004170 ; SwiftTest.test(inout Swift.Int) -> () at main.swift:451
0x100003c5b <+75>: movq -0x28(%rbp), %rdi
0x100003c5f <+79>: leaq 0x8562(%rip), %r13 ; SwiftTest.p : SwiftTest.Point
0x100003c66 <+86>: callq 0x100003d60 ; SwiftTest.Point.y.setter : Swift.Int at main.swift:437
inout的本质总结
如果实参有物理地址,且没有设置属性观察器:
- 直接将实参的内存地址传入函数(实参进行引用传递)
如果实参是计算属性或者设置了属性观察器,采取了Copy In Copy Out的做法:
- 调用该函数时,先复制实参的值,产生副本【get】
- 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值
-
类型属性(Type Property)
严格来说属性可分为:
实例属性(Instance Property):只能通过实例去访问 存储实例属性(Store Instance Property):存储在实例的内存中,每个实例都有1份
- 计算实例属性(Computed Instance Property)
类型属性(Type Property):只能通过类型去访问
- 存储类型属性(Store Type Property):整个程序运行中,就只用1份内存(类似于全局变量)
计算类型属性(Computed Type Property)
struct Point {
// 计算类型属性 x
static var x: Int {
get {
10
}
}
// 存储类型属性 y
static var y: Int = 20
}
可以通过static定一个类型属性
如果是类,也可以用关键词class修饰计算属性
类型属性举例: ```swift struct Car { static var count: Int = 0 init() {Car.count += 1
} }
var c1 = Car() var c2 = Car() var c3 = Car() print(“(Car.count)”) // 3
<a name="g49fC"></a>
## 类型属性的细节
不同于存储实例属性,你必须给存储类型属性设定初始值,因为类型没有实例那样的init初始化器来初始化存储属性。
类型存储类型属性默认就是lazy,会在第一次使用的时候才初始化,就算被多个线程同时访问,保证只会初始化一次。<br />类型存储属性可以是let
枚举类型也可以定义类型属性(存储类型属性、计算类型属性)
<a name="Cs59c"></a>
## 类型属性和全局变量
```swift
var num1 = 10
class Car {
static var count = 10
}
Car.count = 20
var num2 = 12
num1、count、num2都是全局变量,只是count在访问时有限制(需要通过Car去访问),通过查看给count赋值的汇编代码:
SwiftTest`Car.count.unsafeMutableAddressor:
-> 0x100003f40 <+0>: pushq %rbp
0x100003f41 <+1>: movq %rsp, %rbp
0x100003f44 <+4>: cmpq $-0x1, 0x83c4(%rip) ; SwiftTest.num3 : Swift.Int + 7
0x100003f4c <+12>: sete %al
0x100003f4f <+15>: testb $0x1, %al
0x100003f51 <+17>: jne 0x100003f55 ; <+21> at main.swift:514:16
0x100003f53 <+19>: jmp 0x100003f5e ; <+30> at main.swift
0x100003f55 <+21>: leaq 0x83a4(%rip), %rax ; static SwiftTest.Car.count : Swift.Int
0x100003f5c <+28>: popq %rbp
0x100003f5d <+29>: retq
0x100003f5e <+30>: leaq -0x45(%rip), %rsi ; one-time initialization function for count at main.swift
0x100003f65 <+37>: leaq 0x83a4(%rip), %rdi ; one-time initialization token for count
0x100003f6c <+44>: callq 0x1000076e2 ; symbol stub for: swift_once
0x100003f71 <+49>: jmp 0x100003f55 ; <+21> at main.swift:514:16
由于类型存储类型属性默认就是lazy,所以调用了swift_once方法,内部其实是调用了dispatch_once方法,相当于:
dispatch_once {
count = 10
}
当类型属性第一次被访问时是通过dispatch_once去初始化,也保证了线程安全。
单例模式
class FileManager {
// 类型存储属性
public static let shared = FileManager()
private init() {
// do something
}
}
// 访问
let manager = FileManager.shared
static保证内存中只有一份,let保证不会在其他地方修改这个内存,这样就实现了单例功能。