木又的《Swift进阶》读书笔记——结构体和类

774 阅读8分钟

结构体和类

结构体是值类型,而类是引用类型

值类型和引用类型

  • 值类型的本质:赋值意味着按值拷贝。也就是说,每一个值类型变量所持有的值都是独立的。具有这种行为特征的类型被称为具有值语义 (value semantics)
  • 引用类型的本质:变量不含有“事物”本身,而是持有一个对“事物”的引用。其他变量也可以含有对同一个实例的引用,并可以通过任意一个指向它的变量对其做修改。具有这些特性的类型被称为具有引用语义 (reference semantics)

可变性

  • 要理解在属性和变量上用 letvar 的所有不同组合的关键是要记住两点:
    • 类型为类的变量的值,是一个指向实例的引用;而类型为结构体的变量的值,是结构体实例本身。
    • 修改一个结构体的属性,即使修改的是多层的嵌套属性,都等同于给变量赋值一个全新的结构体实例。

可变方法

  • 在结构体上用 func 关键字定义的普通方法,是不能修改结构体的任何属性的。这是因为被隐式传入的 self 参数,默认是不可变的。我们必须明确地使用 mutating func 关键字来创建一个可变方法。

inout 参数

  • 虽然 & 符号可能会让你想起 CObjective-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