Why does this property wrapper code compile?

@propertyWrapper
struct Wrapper {
  var wrappedValue: Int
}

class C {
  @Wrapper
  var value_1: Int {
    didSet {
      print("value_1", oldValue, "->", value_1)
    }
  }

  @Wrapper
  var value_2: Int = 42 {
    didSet {
      print("value_2", oldValue, "->", value_2)
    }
  }
  init(_ value: Int) {
    // #1 ALWAYS IS substituted with `self._value_1 = Wrapper(wrappedValue: value)`
    self.value_1 = value

    // #2 is completely different from #1.
    // this CAN be substituted with: _value_1.wrappedValue = value + 1 
    self.value_1 = value + 1

    // #3 Is similar to #2 rather than #1
    // this CAN be substituted with _value_2.wrappedValue = value
    // because the initialization happens with a default value of 42
    self.value_2 = value
  }
}

let c = C(0)

// prints:
// value_1 0 -> 1
// value_2 42 -> 0

cc @Douglas_Gregor.

Shouldn't this be a compile time error and require me to write self._value_1 = Wrapper(wrappedValue: value) instead?

I don't recall this kind of convenience mentioned in the proposal.

Is this intended or should I file a bug report?

Based on my understanding (I could be wrong), value_1 is a property wrapper variable. Class C is outside the definition of the property wrapper Wrapper.

So value_1 behaves like a property wrapper and accessing (get and set) is using the wrappedValue. So this works with the convenience provided by the property wrapper.

Example 2:
Shows a scenario when initialising the property wrapper variable is possible only using the underscore variable.

@propertyWrapper
struct Wrapper {
    
    //Stored property that needs to be initialised before self.wrappedValue 
    //is accessed because self's properties need to be fully initialised.
    private var underlyingStorage : Int
    
    var wrappedValue: Int {
        get { underlyingStorage }
        set { underlyingStorage = newValue }
    }
    
    init(value: Int) {
        self.underlyingStorage = value
    }
}

class C {
  @Wrapper
  var value_1: Int {
    didSet {
      print("value_1", value_1)
    }
  }

  init(_ value: Int) {

//    self.value_1 = value //Compilation Error: 'self' used in property access 'value_1' before all stored properties are initialized
    _value_1 = Wrapper(value: value) //Compiles ok.
  }
}

As I understand it, the requirement is that the property wrapper's (as with other types I guess) is that all it's properties are initialised before self is used. If all properties are already initialised it is ok to use the wrappedValue.

Hi there, I much appreciate that you tried to explain to me how you think this works, but I already answered what exactly happens in the original post. An init that begins with correct wrappedValue parameter is treated specially by the compiler. Not providing it explicitly just like in your example will imply that this convenience won‘t be allowed by the compiler.

I‘m just surprised why this convenience is allowed, or if it‘s an implication of some other implementation. I seek an answer to these questions only. I don‘t recall the proposal mentioning it, or I just oversee something.

The whole point of property wrappers is that you don’t have to write things like that, you can just use the declared property normally and let the compiler take care of the backing storage automatically.

Your examples appear to be working exactly as designed, intended, and expected.

Well allow me to doubt it, can you point me to a documentation or discussion where #1 is mentioned. The issue here is that the behavior is completely different in all 3 lines because some do one thing and the other trigger a side effect in the given example.

Since I followed every single thread of PW‘s I‘m surprised about this behavior.

Your struct’s default memberwise initializer is named init(wrappedValue:). The Property Wrappers proposal states:

2 Likes

I thought this only means direct assignment of a default value!? :expressionless:

It means any direct assignment to an uninitialized variable, which is what your example #1 uses.

Your examples #2 and #3 are the same as each other. Both properties have already been initialized, so the assignments go through the wrapper as normal.

3 Likes

I do understand what all lines do, I‘m just surprised that I missed that essential behavior until now. Apparently the wording was not enough for me to understand it. Thank you for pointing that out to me.

1 Like