Swift: accessing computed property through pointer

后端 未结 2 2010
伪装坚强ぢ
伪装坚强ぢ 2021-01-22 20:48

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

相关标签:
2条回答
  • 2021-01-22 21:25

    The Swift language definition does not require it to not move (or reuse) the memory used by class object instance properties, once outside the block or scope where unsafe pointers and other internal references are valid.

    So, in your second and third case, the object (or some of its properties) has likely been moved and you are examining and (dangerously) changing memory where the object used to be, and where part of some completely different type of object might currently be, through a stale (and thus very unsafe) pointer.

    So the Swift compiler (which knows when and where it moved stuff) knows how to read and write the property inside the instance. But you (via stale pointers) do not.

    Added: If you want to do this type of stuff, then allocate (and manage) the memory yourself (which is possible in Swift).

    0 讨论(0)
  • 2021-01-22 21:35

    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<T> {
    
        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<T> {
    
        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.

    0 讨论(0)
提交回复
热议问题