结构体和类
结构体是值类型,而类是引用类型。
值类型和引用类型
- 值类型的本质:赋值意味着按值拷贝。也就是说,每一个值类型变量所持有的值都是独立的。具有这种行为特征的类型被称为具有值语义 (value semantics)。
- 引用类型的本质:变量不含有“事物”本身,而是持有一个对“事物”的引用。其他变量也可以含有对同一个实例的引用,并可以通过任意一个指向它的变量对其做修改。具有这些特性的类型被称为具有引用语义 (reference semantics)。
可变性
- 要理解在属性和变量上用 let 和 var 的所有不同组合的关键是要记住两点:
- 类型为类的变量的值,是一个指向实例的引用;而类型为结构体的变量的值,是结构体实例本身。
- 修改一个结构体的属性,即使修改的是多层的嵌套属性,都等同于给变量赋值一个全新的结构体实例。
可变方法
- 在结构体上用
func关键字定义的普通方法,是不能修改结构体的任何属性的。这是因为被隐式传入的self参数,默认是不可变的。我们必须明确地使用mutating func关键字来创建一个可变方法。
inout 参数
- 虽然
&符号可能会让你想起C和Objective-C中的取址操作符,或者是C++中的引用传递操作符,但在Swift中,其作用是不一样的。就像对待普通的参数一样,Swift还是会复制传入的 inout 参数,但当函数返回是,会用这些参数的值覆盖原来的值。也就是说,即使在函数中对一个 inout 参数做多次修改,但对调用者来说只会注意到一次修改的发生,也就是在用新的值覆盖原有值的时候。同理,即使函数完全没有对 inout 参数做任何的修改,调用者也还是会注意到一次修改 (willSet 和 didSet 这两个观察者方法都会被调用)。
生命周期
- 结构体不会有多个所有者。结构体的生命周期,是和含有结构体实例的变量的生命周期绑定的。当变量离开作用域时,其内存将被释放,结构体实例也会被销毁。
- Swift 使用 自动引用计数 (ARC) 来追踪一个实例的引用计数。当引用计数降至 0 时 (例如所有包含引用的变量都离开了作用域,或被设置成了 nil),Swift 运行时会调用对象的 deinit 方法并释放内存。因此,对那些在最终被释放时需要执行清理工作的共享对象,是可以用类来实现的,像是文件句柄 (必须在某个时间点关闭底层的文件描述符),或是
view controller(可能需要做各种清理工作,例如,注销观察者)。
循环引用
- 当两个或多个对象互相之间有强引用的时候,就会产生循环引用,它会让这些对象都无法被释放 (除非开发者显式地打破这种循环)。这会造成内存泄漏,并让那些潜在的清理任务无法执行。
- 产生循环引用的情况可以有多种:从两个对象互相之间强引用吗,到由许多对象组成的复杂循环,以及在闭包中捕获对象。
弱引用
- 为了打破循环引用,我们需要使其中一个引用变为弱引用或
unowned引用。把一个对象赋值给一个弱引用变量,并不会改变实例的引用计数。在 Swift 里,弱引用变量是 归零 (zeroing) 的:一旦所指向的的对象被销毁,变量会自动被设置为 nil。这也是为什么弱引用变量必须是可选值的原因。 - 当使用代理 (delegate) 时,弱引用是非常有用的,并且这在 Cocoa 中很常见。代理对象 (例如,一个 table view) 需要一个指向它的代理的引用,但它不应该拥有代理,否则就可能会产生一个循环引用。因此,指向代理的引用通常都是弱引用,而另一个对象 (例如,一个view controller) 的职责就是确保代理对象在需要的的时候确实存在。
Unowned 引用
- 有时候,我们希望一个引用既是弱引用,但同时又不是一个可选值。对于这种情况,可以使用
unowned关键字。 - 对于
unowned引用,我们的责任是,确保“被引用者”的生命周期比“引用者”要长。 - 在对象中,Swift 运行时使用另外一个引用计数来追踪 unowned 引用。当对象没有任何强引用的时候,会释放所有资源 (例如, 对其他对象的引用)。然而,只要对象还有 unowned 引用存在,其自身所占用的内存就不会被回收。这块内存会被标记为无效,有时也称作僵尸内存 (zombie memory)。被标记为僵尸内存之后,只要我们尝试访问这个
unowned引用,就会发生一个运行时错误。
闭包和循环引用
- 在 Swift 中,类不是唯一的引用类型。函数 (也包括闭包) 同样也是引用类型。如果一个闭包捕获了一个引用类型的变量,那么在闭包中会持有一个对这个变量的强引用。
- 可以使用一个捕获列表,在闭包中显式地控制如何捕获值(比如 weak 或 unowned)
在 unowned 引用和弱引用之间做选择
- 如果对象具有独立的生命周期 (也就是说,你不能保证哪一个对象存在的时间会比另一个长),那么弱引用是唯一安全的选择。
- 另一方面,如果可以保证,非强引用的对象与持有这个引用的对象的生命周期是一样的,甚至于更长的话,unowned 引用通常是更方便的。因为它的类型不需要时可选值,并可以被声明为 let,而弱引用则必须是用 var 声明的可选值。生命周期相同的情况是很常见的,特别当两个对象之间是父子关系时。当父对象使用强引用来控制其子对象的生命周期,并且我们可以保证没有其他对象知道子对象存在的话,子对象对父对象的引用就可以是 unowned。
- 相比弱引用,unowned 引用的开销也小一点,通过它访问属性或调用方法对的速度会快一点点。不过,应该只在对效率非常敏感的代码路径中,才把这个优点作为考虑的因素之一。
- 使用 unowned 引用的缺点也很明显,如果错误地判断了对象的生命周期,程序很可能会崩溃。就个人而言,即使可以用 unowned 引用,我们也经常发现自己更喜欢用弱引用。
在结构体和类之间做抉择
- 要共享一个实例的所有权的话,我们必须使用类。否则,我们可以使用结构体。
具有值语义的类
-
首先我们把所有的属性都声明为 let,使它们都变成不可变。其次,为了避免因为子类而重新引入任何可变行为,我们把类标记为
final来禁止子类化:final class ScoreClass { let home: Int let guest: Int init(home: Int, guest: Int) { self.home = home self.guest = guest } }
具有引用语义的结构体
- 在结构体中存储引用时要非常小心,因为这样做通常都会导致意外的行为。但在某些情况下,是有意这么做的,并且也是你所需要的效果。
写时复制优化
- 写时复制的意思是,在结构体中的数据,一开始是在多个变量之间共享的:只有在其中一个变量修改了它的数据时,才会产生对数据的复制操作。
写时复制的权衡
- 值类型的一个优点就是它们不会产生引用计数方面的开销。但是,因为事先写时复制的结构体,依赖于保存在内部的一个引用,所以这个结构体每产生一份拷贝都会增加这个内部引用的引用计数。实际上,我们是放弃了值类型不需要引用计数的这个优点,来减轻值类型的复制语义这个特性所可能带来的成本。
实现写时复制
struct HTTPRequest {
fileprivate class Storage {
var path: String
var headers: [String: String]
init(path: String, headers: [String: String]) {
self.path = path
self.headers = headers
}
}
private var storage: Storage
init(path: String, headers: [String: String]) {
storage = Storage(path: path, headers: headers)
}
}
extension HTTPRequest.Storage {
func copy() -> HTTPRequest.Storage {
print("Making a copy...") // 调试语句
return HTTPRequest.Storage(path: path, headers: headers)
}
}
extension HTTPRequest {
private var storageForWriting: HTTPRequest.Storage {
mutating get {
if !isKnownUniquelyReferenced(&storage) {
self.storage = storage.copy()
}
return storage
}
}
var path: String {
get { return storage.path }
set {}
}
var headers: [String: String] {
get { return storage.headers }
set {}
}
}
var req = HTTPRequest(path: "/home", headers: [:])
var copy = req
for x in 0..5 {
req.headers["X-RequestId"] = "\(x)"
} // Making a copy