CopyOnWrite

封装 CopyOnWrite 类型的微框架,让实现值语义变得容易!

codecov.io License

为什么你需要它?

CopyOnWrite 是一个微框架,它为在包含引用类型的结构体中实现值语义提供了一个抽象。

值语义是一个概念,它适用于这样一种类型:你不能通过改变另一个变量来影响一个变量的值。任何改变某个变量状态的尝试都将保持在该变量的局部范围内。例如,String 类型是一种具有值语义的值类型(结构体)。

var s1 = "Foo"
var s2 = s1
s1.append("Bar")
print(s1) // FooBar
print(s2) // Foo

s1 赋值给 s2 的操作实际上创建了 s1 内容的副本,并将该副本存储到 s2 中,从而使对其中一个的更改与另一个隔离,这是定义类型是否具有值语义的关键细节。

然而,仅仅因为 String 是一个结构体并不意味着它能免费获得值语义。如果你的结构体类型包含内部引用,就像 String 一样,你必须有意识地、有目的地实现这种行为。考虑以下示例:

class Foo {
  var num: Int = 0
}

struct Bar {
  private var storage = Foo()

  var num: Int {
    return storage.num
  }

  func update(_ num: Int) {
    storage.num = num
  }
}

var bar1 = Bar()
var bar2 = bar1
bar1.update(100)
print(bar1.num) // 100
print(bar2.num) // 100

由于 Bar 结构体包含引用类型,因此在将 bar1 赋值给 bar2 时,引用会被复制,但内存中的实例保持不变,因此更改该实例的属性将影响对该实例的所有引用,这对于使用你的类型的人来说可能是意想不到的行为。有关更深入的详细信息,请阅读这篇文章并查看它引用的资源。

你可以使用这个 CopyOnWrite 库轻松实现值语义,以便在结构体中更改引用类型时,这些更改将保持在存储该结构体的特定变量的局部范围内,而不会影响结构体可能已存储的其他位置。

它是如何工作的?

以下列方式修改前面的示例将使 Bar 类型具有值语义。

class Foo {
  var num: Int = 0
}

struct Bar {
  private var storage = CopyOnWrite(reference: Foo(), copier: {
    let new = Foo()
    new.num = $0.num
    return new
  })

  var num: Int {
    return storage.reference.num
  }

  mutating func update(_ num: Int) {
    storage.mutatingReference.num = num
  }
}

var bar1 = Bar()
var bar2 = bar1
bar1.update(100)
print(bar1.num) // 100
print(bar2.num) // 0

CopyOnWrite 的主要初始化器接受两个参数:要包装的对象,以及一个复制器闭包,该闭包仅在确定需要复制以保留值语义时才被调用。如果只有一个引用指向 CopyOnWrite 包装的内部引用,则不需要调用闭包,它将直接更新引用作为优化。一旦有多个值引用内部存储,CopyOnWrite 将检测到这一事实,并在更改引用之前运行复制闭包。

var bar3 = Bar()
bar3.update(42) // only one reference, no copy made
var bar4 = bar3
bar3.update(1024) // two references to Bar's internal storage in bar3 and bar4, so run the copy closure before making the change
bar3.update(0) // bar3 has a unique reference now after the previous copy, so it will not copy again

用法

引用访问

一旦你初始化了写时复制值,你就可以通过两个属性访问内部引用:

正如你在上面的示例中看到的,Bar 上的 update 方法被更改为 mutating。这是因为简单地访问 mutatingReference 可能会导致 CopyOnWrite 中的引用被重新赋值,因此任何引用此属性的内容也必须标记为 mutating,这有助于保证如果你的结构体存储在 let 常量中,你不会意外更改引用类型中的值。

注意事项

然而,在正确的时间调用正确的属性是你的责任。编译器或此类型都无法阻止你这样做。

func update(_ num: Int) {
  storage.reference.num = num
}

虽然这段代码可以编译,但它会导致最初的问题再次出现,即在一个变量上调用 update 会影响存储在另一个变量中的值。

强烈建议你的写时复制实例变量是私有的,这样你的 API 的外部客户端在尝试直接访问它时就不会做错事。提供函数/属性来暴露你想要的功能,并以引用的对象作为支持。

便利初始化器

该库还提供了一个协议,你可以选择遵循该协议,以防你发现自己在类型中的许多地方重复相同的复制闭包。

public protocol Cloneable: class {
  func clone() -> Self
}

如果你的类型可以遵循该协议,你可以像这样简单地将引用提供给 CopyOnWrite

extension Foo: Cloneable {
  func clone() -> Self {
    let new = self.init()
    new.num = num
    return new
  }
}

struct Bar {
  private var storage = CopyOnWrite(reference: Foo())

  // ...
}

对于可能遵循 NSCopyingNSMutableCopying 的类型,也有针对这些类型的便利初始化器。

CopyOnWrite(copyingReference: MyNSCopyingType())
CopyOnWrite(mutableCopyingReference: MyNSMutableCopyingType())

注意为你要存储的正确类型使用正确的初始化器。如果你为像 NSMutableString 这样的类型使用 NSCopying 版本,则调用的 copy 方法实际上会创建一个不可变版本,当你尝试在其上调用可变方法时,你的程序将会崩溃。

要求

安装

CocoaPods

CopyOnWrite 可通过 CocoaPods 获得。要安装它,只需将以下行添加到你的 Podfile 中:

pod 'CopyOnWrite', '~> 1.0.0'

Carthage

CopyOnWrite 可以与 Carthage 集成。将以下内容添加到你的 Cartfile 以使用它:

github "klundberg/CopyOnWrite" ~> 1.0.0

Swift Package Manager

将以下行添加到你的 Package.swift 文件中的依赖项列表中:

.package(url: "https://github.com/klundberg/CopyOnWrite.git", from: "1.0.0"),

手动安装

只需将 CopyOnWrite.swift 文件复制到你的项目中。

作者

Kevin Lundberg, kevin at klundberg dot com

贡献

如果你有任何想要看到的更改,请随时打开 pull request。请为任何更改包含单元测试。

许可证

CopyOnWrite 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。