CopyOnWrite 1.0.0

CopyOnWrite 1.0.0

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最后发布2017年10月
SwiftSwift 版本4.0
SPM支持 SPM

Kevin Lundberg 维护。



CopyOnWrite μ框架封装了 CopyOnWrite 类型,使得实现值语义变得容易!







为什么你需要它?

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

用法

引用访问

初始化一个副本后,您可以使用两个属性访问内部引用

  • reference - 用于不可变操作:任何不会改变存储的引用类型可观察状态的属性或方法都可以安全地通过此属性调用。
  • mutatingReference - 用于可变操作。任何改变了引用的可观察状态的都必须通过此属性进行调用。

如上图所示,Bar中的update方法已被更改为mutating。这是因为简单访问mutatingReference可能会导致CopyOnWrite中的引用被重新分配,因此引用此属性的所有内容都必须也是可变的,这有助于保证你不会意外地更改你的引用类型中的值,如果你的结构体存储在let常量中。

注意事项

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

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

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

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

便利初始化器

此库还提供了一个你可以选择的规范,以防你发现自己正在你的类型的许多地方重复相同的CopyOnWrite闭包。

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的方法实际上会创建一个不可变版本,当你尝试调用其可变方法时,你的程序会崩溃。

要求

  • 1.0.0 支持Xcode 9/Swift 4
  • 0.9.0 支持Xcode 8.0+/Swift 3.0+
  • iOS 8+/OS X 10.9+/watchOS 2+/tvOS 9+

安装

Swift包管理器

将以下行添加到你的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文件。