Doesn't wrapped optional property get nil as default?

Hi, I want a property of type Int? to not have a negative value. I try to do it this way:

@propertyWrapper
struct NNONWrapper {
    private var _num: Int?
    var wrappedValue: Int? {
        get { _num }
        set {
            if let newValue = newValue {
                _num = newValue < 0 ? nil : newValue
            } else {
                _num = nil
            }
        }
    }
    init() {}
    init(wrappedValue: Int?) {
        self.wrappedValue = wrappedValue
    }
}

struct NonNegOrNil {
    @NNONWrapper var num: Int?
}

var aNum = NonNegOrNil(num: nil)
if let num = aNum.num { print(num) } else { print("nil num") }

to which Xcode 12.5 beta complains, 'nil' is not compatible with expected argument type 'NNONWrapper' on the var aNum line.

Case 1: if I comment out init(){}, then the code compiles and runs as expected.

Case 2: if I leave init(){} in, but give num a default value of nil, as in, @NNONWrapper var num: Int? = nil, then the code also compiles and runs as expected.

Question 1: for Case 1, it seems like NonNegOrNil(num: nil) is being treated the same as NonNegOrNil() even though I have an argument label specified in the code?

Question 2: Case 2 is actually how I thought my code should be behaving, except with the default value of nil being auto assigned when the variable is declared optional. What am I missing?

Question 3: If instead of @NNONWrapper var num: Int? = nil, I use @NNONWrapper(wrappedValue: nil) var num: Int?, I get the same error as the original case. Shouldn't the two variants both call the same initializer and behave the same?

Question 4: I saw this post about using ExpressibleByNilLiteral to address a similar problem. Would the use of ExpressibleByNilLiteral be applicable here?

Thanks.

3 Likes

An optional wrapped property only gets a default = nil if the property wrapper does not use default initialization via init(). This is why commenting out the default initializer allows the code to compile.

The compiler is choosing the default initializer of NNONWrapper to initialize NonNegOrNil.num. When the default initializer is used, the synthesized memberwise initializer for the enclosing type will use the backing property wrapper, not the wrapped-value type. Since the wrapper type is not ExpressibleByNilLiteral, that's why the error occurs.

EDIT: Note that the compiler will use the same argument label for a wrapped property in the generated memberwise initializer even if it chooses the property wrapper type rather than the wrapped value type (which I personally find a bit confusing).

The default wrapper initializer is given higher priority over the default = nil in the case where both are possible. The code you wrote is equivalent to this

struct NonNegOrNil {
    @NNONWrapper() var num: Int? // calls NNONWrapper.init()
}

No, these two variants are not the same - if you initialize the wrapper explicitly using only arguments in the wrapper attribute without = <some wrapped value> or omitting the wrapped value entirely, the compiler will still choose the backing wrapper type for the generated member-wise initializer. For the compiler to use the wrapped-value type in the generated memberwise initializer, it has to have spliced the wrappedValue argument into the initializer call.

Before I answer this - why do you need the default initializer for the wrapper? I realize that you may have reduced the example for the purpose of this question, but the actual case you're working on may be more complicated :slightly_smiling_face:

3 Likes

Thanks for the answers, Holly.

As for Question 4, I'm trying to populate a struct from network input, some of which values may be nil/null .

I could check for nil before constructing the struct , but was trying to do the check inside the property wrapper instead. Hence the passing of nil value to the initializer.

I'm not sure that answers your question.

I tried using ExpressibleByNilLiteral as follows:

@propertyWrapper
struct NNONWrapper<T> where T: ExpressibleByNilLiteral {
    private var _num: T = nil
    var wrappedValue: T {
        get { _num }
        set { _num = newValue }
    }
    init() {}
    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }
}

struct NonNegOrNil {
    @NNONWrapper<Int?> var num: Int? = nil
    //@NNONWrapper<Int?>(wrappedValue: nil) var num: Int?
}

I simplified the setter from the original to avoid handling "newValue not being of optional type" and "T not conforming to BinaryInteger." I also had to manually set _num to default value of nil as T itself is not an optional.

But this generic version still suffers all the same issues the original version did. Unless there's another way to write the generic version, I'd conclude that even for a generic property wrapper, "An optional wrapped property only gets a default = nil if the property wrapper does not use default initialization via init()." As far as I can tell, this is not documented in the "Properties" and/or "Initialization" sections of the Swift Language Guide?

My question is specifically, why not write your property wrapper without init(), e.g.

@propertyWrapper
struct NNONWrapper {
    private var _num: Int?
    var wrappedValue: Int? {
        get { _num }
        set {
            if let newValue = newValue {
                _num = newValue < 0 ? nil : newValue
            } else {
                _num = nil
            }
        }
    }

    // Removed init() here

    init(wrappedValue: Int?) {
        self.wrappedValue = wrappedValue
    }
}

because this wrapper can still be implicitly default initialized via the implicit = nil when the wrapper is applied, and the nil will get passed to init(wrappedValue:).

Yes, default property wrapper initialization via init() is always preferred over any other form of default initialization of the wrapped value type that the language supports (which would then get passed to init(wrappedValue:)).

I just did a quick search and also could not find any documentation in The Swift Programming Language about how property wrappers interact with memberwise initializers. There are also special rules when init(wrappedValue:) takes in an @autoclosure. Please feel free to file a bug about this!

1 Like

Thanks for the confirmation. I'm indeed writing the wrapper without a default init now. I had it in there leftover from when the wrapped value was not optional.

I'll file a bug report about the documentation. Thanks.

1 Like