Swift 帮助我们编写更健壮的代码的方式之一是通过其价值类型的概念,它限制了状态可以跨 API 边界共享的方式。这是因为,当使用值类型时,所有的突变(默认)只对我们正在处理的值的本地副本执行,而实际执行突变的API必须明确标记为mutating 。
在这篇文章中,让我们来探讨一下这个关键词,以及它的nonmutating 对应词,以及这些语言特性所提供的那种能力。
变异函数能做什么?
从本质上讲,一个被标记为mutating 的函数可以改变其包围的值中的任何属性。这里 "值 "这个词真的很关键,因为Swift的结构化突变概念只适用于值类型,而不是像类和角色这样的引用类型。
例如,下面这个Meeting 类型的cancel 方法是突变的,因为它修改了其包围类型的state 和reminderDate 属性:
struct Meeting {
var name: String
var state: MeetingState
var reminderDate: Date?
...
mutating func cancel(withMessage message: String) {
state = .cancelled(message: message)
reminderDate = nil
}
}
除了修改属性,突变的上下文还可以给self 赋予一个全新的值,这在给一个枚举(不能包含任何存储的实例属性)添加突变的方法时非常有用。例如,在这里我们要创建一个API,使其能够轻松地将一个Operation 。
enum Operation {
case add(Item)
case remove(Item)
case update(Item)
case group([Operation])
}
extension Operation {
mutating func append(_ operation: Operation) {
self = .group([self, operation])
}
}
上述技术也适用于其他的值类型,比如结构,如果我们想把一个值重设为它的默认属性集,或者我们想把一个更复杂的值作为一个整体进行变异--比如说像这样,这就非常有用:
struct Canvas {
var backgroundColor: Color?
var foregroundColor: Color?
var shapes = [Shape]()
var images = [Image]()
mutating func reset() {
self = Canvas()
}
}
我们可以在突变函数中为self 赋予一个全新的值,这一事实最初可能看起来有点奇怪,但我们必须记住,Swift 结构实际上只是值而已--所以就像我们可以通过赋予一个新的数字来替换Int 值,我们也可以对任何其他结构(或枚举)做同样的事情。
变异协议要求
尽管分离突变和非突变API的概念是价值类型所特有的,但我们仍然可以使一个mutating 函数成为协议的一部分--即使该协议最终可能被一个引用类型(如类)所采用。类在符合这样的协议时可以简单地省略mutating 关键字,因为它们本身就是可变的。
不过,非常有趣的是,如果我们用一个默认的变异函数实现来扩展一个协议,那么我们就可以实现像上面的reset API那样的东西,而不需要知道我们要重置的是什么类型的值--像这样:
protocol Resettable {
init()
mutating func reset()
}
extension Resettable {
mutating func reset() {
self = Self()
}
}
struct Canvas: Resettable {
var backgroundColor: Color?
var foregroundColor: Color?
var shapes = [Shape]()
var images = [Image]()
}
在初始化器中执行突变
当我们想修改一个值类型的内部状态(无论是一个属性,还是整个值本身)时,函数总是需要明确标记为突变的,而初始化器默认总是突变的。这意味着,除了给一个类型的属性分配初始值外,初始化器还可以调用变异方法来执行它的工作(只要self ,事先已经完全初始化了)。
例如,下面的ProductGroup 调用它自己的add 方法,以便添加所有被传入其初始化器的产品--这使得我们有可能为该逻辑使用单一的代码路径,无论它是否作为初始化过程的一部分被运行:
struct ProductGroup {
var name: String
private(set) var products = [Product]()
private(set) var totalPrice = 0
init(name: String, products: [Product]) {
self.name = name
products.forEach { add($0) }
}
mutating func add(_ product: Product) {
products.append(product)
totalPrice += product.price
}
}
就像变异函数一样,初始化器也可以直接给self 。请看这个快速提示中的一个例子。
非变异属性
到目前为止,我们所看的所有例子都是关于可变型上下文的,但Swift也提供了一种方法来将某些上下文明确标记为非变型。虽然与选择突变相比,这样做的用例肯定更加有限,但在某些情况下,它仍然是一个有用的工具。
作为一个例子,让我们看一下这个简单的SwiftUI视图,它在每次点击按钮时都会增加一个@State-markedvalue 属性:
struct Counter: View {
@State private var value = 0
var body: some View {
VStack {
Text(String(value)).font(.largeTitle)
Button("Increment") {
value += 1
}
}
}
}
现在,如果我们不只是把上面的内容作为一个SwiftUI视图来看,而是作为一个标准的Swift结构(它就是这样),那么我们的代码能被编译出来其实是很奇怪的。为什么我们可以像这样在一个闭包中突变我们的value 属性,而不是在一个同步的、可突变的上下文中被调用?
如果我们再看看State 属性包装器的声明,谜团就更大了,它也是一个结构,就像我们的视图一样:
@frozen @propertyWrapper public struct State<Value>: DynamicProperty {
...
}
那么,为什么一个基于结构的属性包装器,在基于结构的视图中使用,实际上可以在非变异的上下文中被变异呢?答案就在State 包装器的wrappedValue 的声明中,该声明已经用nonmutating 关键字标记:
public var wrappedValue: Value { get nonmutating set }
虽然这只是我们在没有访问 SwiftUI 源代码的情况下能够调查的范围,但State 很可能在后台使用了某种形式的基于引用的存储,这反过来使它有可能选择不使用 Swift 的标准值突变语义(使用nonmutating 关键字)--因为当我们分配一个新属性值时,State 封装器本身实际上并没有被突变。
如果我们愿意,我们也可以将这种能力添加到我们自己的一些类型中。为了证明这一点,下面的PersistedFlag 包装器使用UserDefaults 来存储它的底层Bool 的值,这意味着当我们给它分配一个新的值时(通过它的wrappedValue 属性),我们实际上也没有在这里进行任何基于值的变迁。因此,该属性可以被标记为nonmutating ,这使PersistedFlag 具有与State 相同的变异能力。
@propertyWrapper struct PersistedFlag {
var wrappedValue: Bool {
get {
defaults.bool(forKey: key)
}
nonmutating set {
defaults.setValue(newValue, forKey: key)
}
}
var key: String
private let defaults = UserDefaults.standard
}
所以就像@State 标记的属性一样,任何我们用@PersistedFlag 标记的属性现在都可以被写入非突变的环境中,比如在转义闭包中。但需要注意的是,nonmutating 关键字让我们规避了 Swift 价值语义的关键方面,所以它肯定只能在非常特殊的情况下使用。
总结
我希望这篇文章能给你一些启示,让你了解什么是变异上下文和非变异上下文的区别,变异上下文到底有什么样的能力,以及Swift相对较新的nonmutating 关键字的作用。
谢谢你的阅读!