I was playing around with trying to get/set a computed property of an object through a pointer to that property. I\'ve included the code snippet and output below.
The g
None of this is defined behaviour. It may or may not produce expected results, or it may just crash at runtime.
When you say
let m1 = Mutator(name:"mf1", storage: &f1.bar)
Swift will allocate some memory and initialise it to the value returned by f1.bar
's getter. A pointer to this memory will then be passed into Mutator
's init
– and after the call, Swift will then call f1.bar
's setter with the (possibly changed) contents of the memory it allocated.
This memory will then be deallocated – the pointer is now no longer valid. Reading and writing to its pointee
will produce undefined behaviour. Therefore, you should not persist the pointer after the call to Mutator
's initialiser.
One way in order to get the behaviour you want is to use two closures for the getting and setting of f1.bar
, both capturing f1
. This ensures that the reference to f1
remains valid as long as the closures live.
For example:
struct Mutator {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get {
return getter()
}
nonmutating set {
setter(newValue)
}
}
init(getter: @escaping () -> T, setter: @escaping (T) -> Void) {
self.getter = getter
self.setter = setter
}
}
You can then use it like so:
class Foo {
private var data = [String : Double]()
var bar: Double? {
get { return self.data["bar"] }
set { self.data["bar"] = newValue }
}
init(_ key: String, _ val: Double) {
self.data[key] = val
}
}
let f1 = Foo("bar", 1.1)
let m1 = Mutator(getter: { f1.bar }, setter: { f1.bar = $0 })
let before = m1.value
m1.value = 199.1
print("m1: before = \(before as Optional), after = \(m1.value as Optional)")
print("f1 after = \(f1.bar as Optional)")
// m1: before = Optional(1.1000000000000001), after = Optional(199.09999999999999)
// f1 after = Optional(199.09999999999999)
Although one downside to this approach is the repetition of value you're getting and setting (f1.bar
in this case). One alternative implementation would be to use a single closure with a function argument that takes an inout
parameter, returning the (possibly mutated) value.
struct Mutator {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get {
return getter()
}
nonmutating set {
setter(newValue)
}
}
init(mutator: @escaping ((inout T) -> T) -> T) {
// a function, which when applied, will call mutator with a function input
// that just returns the inout argument passed by the caller.
getter = {
mutator { $0 }
}
// a function, which when applied with a given new value, will call mutator
// with a function that will set the inout argument passed by the caller
// to the new value, which will then be returned
// (but ignored by the outer function)
setter = { newValue in
_ = mutator { $0 = newValue; return $0 }
}
}
}
// ...
let f1 = Foo("bar", 1.1)
let m1 = Mutator { $0(&f1.bar) }
The getter now simply applies the passed function, returning the inout
parameter passed (f1.bar
in this case), and the setter uses this inout
parameter in order to assign a new value.
Although personally, I prefer the first approach, despite the repetition.