Mutating and non-mutating Swift contexts
Swift帮助我们编写更健壮代码的方法之一是它的值类型概念,它限制了状态可以跨API边界共享的方式。这是因为,当使用值类型时,所有的mutations(默认情况下)只对我们正在处理的值的本地副本执行,并且实际执行mutations api必须清楚地标记为mutating。
在本文中,让我们探讨这个关键字及nonmutating, 以及这些语言特性所提供的功能。
What can a mutating function do?
本质上,标记为mutating的函数可以更改其外围值中的任何属性。 mutating这个词在这里非常关键,因为Swift的结构化mutations概念只适用于值类型,而不适用于引用类型,如类和actor。
例如,下面的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,让添加一个操作到另一个操作变得容易:
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的structs实际上只是值 - 就像我们可以通过给一个Int值赋一个新数字来替换它一样,我们也可以对任何其他struct(或enum)做同样的事情。
Mutating protocol requirements
虽然区分mutatingapi和non-mutating的api的概念是值类型所特有的,但我们仍然可以将mutating函数作为协议的一部分,即使该协议最终可能被引用类型(如类)所采用。类在遵循这样的协议时可以简单地省略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]()
}
Performing mutations within initializers
当我们想要修改值类型的内部状态时(无论是属性还是整个值本身),函数总是需要显式地标记为mutating,默认情况下初始化器总是在mutating。这意味着,除了给类型的属性赋初始值外,初始化器还可以调用突变方法来执行它的工作(只要self已经事先完全初始化)。
例如,下面ProductGroup调用自己的添加方法,以将所有的产品传递到其初始化—— 这使得我们可以使用单一的代码路径来实现逻辑,无论它被作为初始化过程的一部分运行与否:
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
}
}
Non-mutating properties
到目前为止,我们看到的所有示例都是关于可变上下文的,但Swift也提供了一种方法来标记某些上下文为明确的non-mutating 。 虽然与选择mutations相比,这样做的用例肯定更有限,但在某些情况下,它仍然是一个有用的工具。
举个例子,让我们看看这个简单的SwiftUI视图,它在每次点击按钮时都会增加@ state标记的value属性:
struct Counter: View {
@State private var value = 0
var body: some View {
VStack {
Text(String(value)).font(.largeTitle)
Button("Increment") {
value += 1
}
}
}
}
现在,如果我们不仅仅把上面的看作一个SwiftUI View,而是看作一个标准的Swift struct(它确实是),那么我们的代码编译起来会很奇怪。 为什么我们可以在闭包中这样改变value属性,而不是在同步的,可变的上下文中调用?
如果我们再看一下State属性包装器的声明,就会更加神秘,它也是一个结构体,就像我们的视图:
@frozen @propertyWrapper public struct State<Value>: DynamicProperty {
...
}
那么,在基于struct的视图中使用的基于struct的属性包装器,如何能够在non-mutating的上下文中发生变化呢? 答案就在State包装器的wrappedValue的声明中,该值已经用nonmutating关键字标记:
public var wrappedValue: Value { get nonmutating set }
尽管这是我们目前为止在没有访问SwiftUI源代码的情况下所能研究的,State很可能在内部使用了某种形式的基于引用的存储, 这反过来又使得它可以选择不使用Swift的标准值mutation 语义(使用nonmutating关键字)——因为当我们赋值一个新的属性值时,State包装器本身实际上并没有被改变。
如果我们愿意,我们也可以将这个功能添加到我们自己的一些类型中。为了演示,下面的persistdflag包装器使用UserDefaults存储其底层的Bool值,这意味着当我们给它赋一个新值时(通过它的wrappedValue属性),我们实际上也没有在这里执行任何基于值的更改。因此,该属性可以被标记为nonmutating,这给了persistdflag与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-marked属性一样,任何我们用@PersistedFlag标记的属性现在都可以被写入,即使是在non-mutating上下文中,比如在转义闭包中。 但是,需要注意的是,nonmutating 关键字可以让我们绕过Swift值语义的关键方面,所以它绝对应该只在非常特定的情况下使用。
Conclusion
我希望这篇文章能让您对如何区分mutating上下文和nonmutating上下文、mutating上下文实际上具有什么样的功能以及Swift相对较新的nonmutating关键字做什么有一些了解。