I am following example code from the book "Design Patterns in Swift 5". The example is for the section on the State Pattern.
I am getting a memory access error: "Simultaneous accesses to 0x14572f2a0, but modification requires exclusive access." when I try to run the code.
I am not sure what's going on... Thanks in advance!
Code:
import Foundation
public final class ATM {
fileprivate var state: ATMState = IdleState()
public func enter(pin: String) {
state = EnterPinState(context: self)
state.validate(pin: pin)
}
public func withdraw(amount: Float) -> Bool {
return state.withdraw(amount: amount)
}
}
fileprivate protocol ATMState {
func validate(pin: String)
mutating func withdraw(amount: Float) -> Bool
func transactionCompleted(success:Bool)
}
extension ATMState {
func validate(pin:String) {
print("not implemented")
}
mutating func withdraw(amount: Float) -> Bool {
print("not implemented")
return false
}
func transactionCompleted(success:Bool) {
print("not implemented")
}
}
fileprivate struct IdleState: ATMState {
}
fileprivate struct EnterPinState: ATMState {
let context: ATM
func validate(pin: String) {
guard pin == "1234" else {
print("wrong pin")
context.state = TransactionCompleteState(context:context)
context.state.transactionCompleted(success: false)
return
}
print("pin ok")
context.state = WithdrawState(context: context)
}
}
fileprivate struct TransactionCompleteState: ATMState {
let context: ATM
func transactionCompleted(success: Bool) {
var statusMessage = success ? "Transaction complete..." : "Transaction failed..."
print(statusMessage)
context.state = IdleState()
}
}
fileprivate struct WithdrawState: ATMState {
var context: ATM
var availableFunds: Float = 1000
init(context: ATM) {
self.context = context
}
mutating func withdraw(amount: Float) -> Bool {
print("Withdraw $\(amount)")
guard amount > 0 && availableFunds >= amount else {
print("invalid")
context.state = TransactionCompleteState(context: context)
context.state.transactionCompleted(success: false)
return false
}
availableFunds -= amount
context.state = TransactionCompleteState(context: context)
context.state.transactionCompleted(success: true)
return true
}
}
let atm = ATM()
atm.enter(pin:"1234")
atm.withdraw(amount: 10)
CodePudding user response:
This is what you get for testing the code in a playground — there's no debugger! If you try that code in a real app, the debugger tells you exactly what the problem is.
The problem is here:
public func withdraw(amount: Float) -> Bool {
return state.withdraw(amount: amount) // <-- *
}
The withdraw state's withdraw
method sets the state
property of its context
, which is this ATM instance. So the call to state.withdraw
means that we both mutate this ATM's state value directly, by replacing it with a different state object, and call its mutating method withdraw
, in a single move. You can't do that, because the double mutation, to a property of the struct and to a reference to the struct as a whole, is incoherent. You have to proceed in stages, like this:
public func withdraw(amount: Float) -> Bool {
// fetch
var theState = self.state
// mutate
return theState.withdraw(amount: amount)
}
The alternative would be to make all of the state types classes instead of structs. A class, unlike a struct, is mutable in place. That would require considerably more rewriting, however, so that is left as an exercise for the reader.