swiftassignment-operatorvalue-typemutabilitymutating-function

Mutable Property that Prohibits Direct Assignment - Swift


Variable Mutability in Exclusion of Assignment

Swift 5.0 - Xcode 10.2.1

I have a class with a property that I would like to be mutable, except for that I don't want to allow the property to be directly assigned with the = operator. Take the following example:

class Foo {
    var counter = Counter()

    init() { }
}

struct Counter {
    private(set) var n = 0

    let interval: Int

    mutating func increment() {
        n += interval
    }

    init(by interval: Int = 1) {
        self.interval = interval
    }
}

What I want to be allowed:

let foo = Foo()
foo.counter.increment()

What I don't want to be allowed:

let fasterCounter = Counter(by: 10)
let foo = Foo()
foo.counter = fasterCounter

Note: While I am aware that I can make counter a private(set) var and create and make an incrementCounter() function in Foo that increments it, I would like to be able to access the mutating methods directly through the counter variable as it clutters the class and is annoying to do for types with many mutating methods. As well, I know I can also make counter a constant and Counter a class, but I need value type semantics for the property.


Solution

  • You’ve correctly listed all the available options. There is no additional trick that you’re unaware of, if that’s what you’re asking. This is just a shortcoming of Swift when (as often happens) you want to use a helper struct.

    I encounter this issue all the time in my own code. For example:

    final class ViewController: UIViewController {
        lazy var state = Mover(owner:self)
    

    This is not what I really wanted to say, for the same reason as you. Mover needs to be mutable and is a struct. So in theory another Mover could be assigned to my state over top of this one. I just have to make a contract with myself not to do that, which, alas, is not easily enforcible.

    If all you really want to do is prevent substitution of a counter with a different interval, you could use a didSet observer (though of course enforcement is at runtime, not compile time):

    var counter = Counter() {
        didSet {
            if oldValue.interval != counter.interval {
                fatalError("hey")
            }
        }
    }
    

    But you can well imagine that this could get very complicated. Every mutation substitutes a different Counter, so precautions to make sure that this substitution is "just" the result of a mutation might have to be quite elaborate. Also if you were going to take that approach, you would probably want to foist off on Counter itself the job of knowing what constitutes a "legal" mutation. In this simple case we could perhaps use equality:

    class Foo {
        var counter = Counter() {
            didSet {
                if oldValue != counter {
                    fatalError("hey")
                }
            }
        }
        init() { }
    }
    
    struct Counter : Equatable {
        static func ==(lhs:Counter,rhs:Counter) -> Bool {
            return rhs.interval == lhs.interval
        }
        // the rest as before....
    }