Swift:`mutating`和`nonmutating`函数的介绍及应用

1,073 阅读6分钟

Swift 帮助我们编写更健壮的代码的方式之一是通过其价值类型的概念,它限制了状态可以跨 API 边界共享的方式。这是因为,当使用值类型时,所有的突变(默认)只对我们正在处理的值的本地副本执行,而实际执行突变的API必须明确标记为mutating

在这篇文章中,让我们来探讨一下这个关键词,以及它的nonmutating 对应词,以及这些语言特性所提供的那种能力。

变异函数能做什么?

从本质上讲,一个被标记为mutating 的函数可以改变其包围的值中的任何属性。这里 "值 "这个词真的很关键,因为Swift的结构化突变概念只适用于值类型,而不是像类和角色这样的引用类型。

例如,下面这个Meeting 类型的cancel 方法是突变的,因为它修改了其包围类型的statereminderDate 属性:

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 关键字的作用。

谢谢你的阅读!