Question?
struct porque: View {
@State private var flag = false
private var anotherFlag = false
mutating func changeMe(_ value: Bool) {
self.anotherFlag = value
}
var body: some View {
Button(action: {
// This is okay
self.flag = true
// Cannot assign to property: 'self' is immutable
self.anotherFlag = true
// Cannot use mutating member on immutable value: 'self' is immutable
changeMe(true)
}) {
Text("Hi")
}
}
}
Why I can mutate @State var, but not regular member var?
Answer
这里(至少)有两个问题:为什么我们可以改变flag,为什么我们不能改变另一个anotherFlag ?
让我们从为什么我们不能改变anotherFlag开始。
下面是一个关于 immutable property 错误的简单例子:
struct SimpleStruct {
var anotherFlag: Bool {
_anotherFlag = true
// ^~~~~~~~~~~~
// error: cannot assign to property: 'self' is immutable
return _anotherFlag
}
private var _anotherFlag = false
}
通常,在属性getter中,self是不可变的,而且通常, 我们不能给不可变值的属性赋值。因此,报错。
但是我说了两遍“usually”,每个“usually”都有一个方法。
第一个usually是,在属性getter中,self是不可变的。我们可以通过声明getter mutating来让self成为mutable:
struct SimpleStruct {
var anotherFlag: Bool {
mutating get {
_anotherFlag = true
return _anotherFlag
}
}
private var _anotherFlag = false
}
self在getter中是可变的,所以Swift允许我们赋值给它的可变属性。 但这有两个问题。首先,我们只能在可变值上使用这个getter:
var s0 = SimpleStruct()
_ = s0.anotherFlag // ok, and modifies s0
let s1 = SimpleStruct()
_ = s1.anotherFlag
// ^~ error: cannot use mutating getter on immutable value: 's1' is a 'let' constant
其次,SwiftUI不允许我们用mutating get声明body :
struct SimpleView: View {
// ^ error: type 'SimpleView' does not conform to protocol 'View'
var body: some View {
mutating get { Text("Hello") }
}
}
View协议要求body的getter是 non-mutating。
第二个“usually”是我们不能给一个immutable值的属性赋值。可以通过声明setter nonmutating使属性可赋值:
struct SimpleStruct {
var anotherFlag: Bool {
get {
_anotherFlag = true
return _anotherFlag
}
}
private var _anotherFlag: Bool {
get { return storage.pointee }
nonmutating set { storage.pointee = newValue }
}
private let storage = UnsafeMutablePointer<Bool>.allocate(capacity: 1)
}
这是有效的,但请注意它的作用类似于引用类型:
let s0 = SimpleStruct()
let s1 = s0
_ = s1.get // effectively mutates both s0 and s1
SwiftUI的State使用nonmutating set。
具体地说,State是一个属性包装器,它的wrappedValue 属性是nonmutating set。所以当你像这样声明flag:
@State private var flag = false
Swift将其转换为三个属性:
private var _flag: State<Bool> = State(initialValue: false)
private var $flag: Binding<Bool> { return _flag.projectedValue }
private var flag: Bool {
get { return _flag.wrappedValue }
nonmutating set { _flag.wrappedValue = newValue }
}
这里的flag属性是nonmutating set,因为State的wrappedValue是nonmutating set。
在内部,SwiftUI为您的flag属性的当前值分配存储空间,并设置State包装器的内部属性来标识该存储空间。您可以通过调用*dump(self._flag)*来了解其内部工作原理。例子:
struct ExampleView: View {
@State var flag = false
init() {
dump(self._flag)
}
var body: some View {
Button("flag = \(flag)" as String, action: { self.flag = true; dump(self._flag) })
}
}
调用dump在init中打印如下:
▿ SwiftUI.State<Swift.Bool>
- _value: false
- _location: nil
内部的*_location属性标识SwiftUI私下存储属性当前值的位置。在你将视图传递给SwiftUI后,它会被初始化。当你点击按钮时,在action闭包中的dump*打印如下:
▿ SwiftUI.State<Swift.Bool>
- _value: false
▿ _location: Optional(SwiftUI.StoredLocation<Swift.Bool>)
▿ some: SwiftUI.StoredLocation<Swift.Bool> #0
- super: SwiftUI.AnyLocation<Swift.Bool>
- super: SwiftUI.AnyLocationBase
▿ viewGraph: Optional(SwiftUI.ViewGraph)
- some: SwiftUI.ViewGraph #1
▿ signal: AttributeGraph.WeakAttribute<()>
- _subgraph: <AGSubgraph 0x60000353c840 [0x7fff805cbf90]> #2
- super: NSObject
▿ _attribute: AttributeGraph.Attribute<()>
▿ identifier: __C.AGAttribute
- id: 36
▿ valueLock: SwiftUI.SpinLock
▿ lock: __C.os_unfair_lock_s
- _os_unfair_lock_opaque: 0
- value: true
▿ savedValues: 1 element
- false
- _wasRead: true
▿ cache: SwiftUI.AtomicVariable<SwiftUI.LocationProjectionCache>
▿ _value: SwiftUI.LocationProjectionCache
- cache: 0 key/value pairs
▿ lock: SwiftUI.SpinLock
▿ lock: __C.os_unfair_lock_s
- _os_unfair_lock_opaque: 0
注意,即使在赋值self.flag = true, _flag._value仍然为false。该*_value属性仅存储该属性的初始值; 它不能在body内部被改变(因为self在body的getter内部是不可变的,因为body的getter没有声明为mutating*)。相反,current value 是在*_flag._location!.value*中找到的。