how to use setter/getters during init()?

124 views Asked by At

I've never quite grasped what's going on with init().

Is there a clean, simple way to achieve what I'm trying to do in the code below ?

Edit: I'd like to avoid duplicating the data = value / 2 logic, because it might be significantly more complex than this simple example. I also don't want to provide a default value for data or make it optional because I want a compile-time guarantee that data is initialized.

fileprivate struct Foo {
    
    var data: Int
    
    var twiceAsMuch: Int {
        set(value) {
            data = value / 2
        }
        get {
            data * 2
        }
        
    }
    
    init(twice: Int) {
        // The following line won't compile because
        // "Self used before all stored properties are initialized"
        self.twiceAsMuch = twice
    }
}
3

There are 3 answers

2
Ron On BEST ANSWER

Edited. This method only works if the calculation does not depend on instance variables.

It's telling you that until "init" has executed, the struct state required for the computed variable is not initialized.

This compiles just fine: (I assumed you wished to divide the initial value inside the initializer.

    
fileprivate struct Foo {
    
    var data: Int
    static private func twiceAsMuch(_ v: Int) -> Int { v / 2 }
    var twiceAsMuch: Int {
        set(value) {
            data = Self.twiceAsMuch(value)
        }
        get {
            data * 2
        }
    }
    
    init(twice: Int) {
        self.data = Self.twiceAsMuch(twice)
    }
}

2
son On

Initialization is the process of allocating memory for any instance, such as class/struct/enum. During this process, each stored property must be assigned an initial value before it's ready to use. Imagine how you can use an instance/variable without knowing where it is, it has to be somewhere in memory, and this 'somewhere' happens during the init function.

fileprivate struct Foo {
    var data: Int
}

By declaring above, you're saying that Foo has data that is stored later. And computed property is nothing like a kind of function, it doesn't actually hold a value. It just provides a getter and a setter to communicate with other properties. Every single time computed properties get called, they will be calculated again. It may take O(1), O(n) or more time depending on the getter and setter. Instead of stored property, it is just a block of memory and it often costs O(1).

So, in short, you're 'communicating' with a memory that wasn't allocated. Then the solution is to 'feed' its instance some memory. There are a couple ways to do it:

  1. Simply give a default value for these stored properties:
fileprivate struct Foo {
    var data: Int = 0
    ...
    init(count: Int) {
        self.doubleCount = count
    }
}
  1. Declare as optional:
fileprivate struct Foo {
    var data: Int?
    ...
    init(count: Int) {
        self.doubleCount = count
    }
}
3
Sweeper On

You can add an init accessor to allow self.twiceAsMuch = twice. This is the accessor that will be called when you set a computed property in init.

twiceAsMuch would look like this:

var twiceAsMuch: Int {
    set(value) {
        data = value / 2
    }
    get {
        data * 2
    }
    
    @storageRestrictions(initializes: data)
    init(value) {
        data = value / 2
    }
}

// now this compiles!
init(twice: Int) {
    self.twiceAsMuch = twice
}

Note that you should indicate which stored properties the initer would initialise. In this case, it's data.

For your simple Foo struct this isn't any more convenient than directly doing self.data = twice / 2. But if you have multiple initialisers that all needs to set twiceAsMuch, and the computation is more complicated than a division by 2, you can avoid repeating the same computation.