Property wrapper init calling diference between direct declaration and extension

I noticed a difference in which init method a property wrapper ends up calling depending on if the more constrained init is defined directly within the property wrapper compared to being defined in an extension.

Any ideas why this would be happening?

Example:

// MARK: - Direct ExpressibleByNilLiteral init

@propertyWrapper
struct Direct<Wrapped> {
    let wrappedValue: Wrapped
    
    init(wrappedValue: Wrapped) {
        print("Direct wrappedValue used")
        self.wrappedValue = wrappedValue
    }
    
    init() where Wrapped: ExpressibleByNilLiteral {
        print("Direct ExpressibleByNilLiteral used")
        self.wrappedValue = nil
    }
}

// MARK: - Extension ExpressibleByNilLiteral init

@propertyWrapper
struct Extended<Wrapped> {
    let wrappedValue: Wrapped
    
    init(wrappedValue: Wrapped) {
        print("Extended wrappedValue used")
        self.wrappedValue = wrappedValue
    }
}

extension Extended where Wrapped: ExpressibleByNilLiteral {
    init() {
        print("Extended ExpressibleByNilLiteral used")
        self.wrappedValue = nil
    }
}

// MARK: - Test

struct Test {
    @Direct var direct: Int?
    @Extended var extended: Int?
}

_ = Test()
// prints:
// Direct ExpressibleByNilLiteral used
// Extended wrappedValue used

It behaves the same if the init within the extension is constrained instead of the extension itself being constrained.

1 Like

This isn't an answer, but interestingly, the overload called for @Direct appears to vary if you explicitly assign nil or not: the explicit assignment of nil leads to wrappedValue being used, and implicit leads to the ExpressibleByNilLiteral overload. With Swift 5.5.2 for me:

struct Test1 {
    @Direct var direct: Int?
}

struct Test2 {
    @Direct var direct: Int? = nil
}

_ = Test1() // => Direct ExpressibleByNilLiteral used
_ = Test2() // => Direct wrappedValue used

The same appears to be true if you replace Int? with another type conforming to ExpressibleByNilLiteral:

struct Foo: ExpressibleByNilLiteral {
    init(nilLiteral: ()) {}
}

struct Test1 { @Direct var direct: Foo }
struct Test2 { @Direct var direct: Foo = nil }

_ = Test1() // => Direct ExpressibleByNilLiteral used
_ = Test2() // => Direct wrappedValue used

I don't know if this is expected, but this definitely seems surprising, and possibly worthy of a report on bugs.swift.org


Also interestingly, and I don't know if this is a related issue or not, but for desugared and non-Optional types, @Extended always appears to require an explicit nil assignment:

struct Foo: ExpressibleByNilLiteral {
    init(nilLiteral: ()) {}
}

struct Test1 { @Extended var extended: Foo }
struct Test2 { @Extended var extended: Foo = nil }
struct Test3 { @Extended var extended: Optional<Int> }
struct Test4 { @Extended var extended: Int? }

_ = Test1() // error: Missing argument for parameter 'extended' in call
_ = Test2()
_ = Test3() // error: Missing argument for parameter 'extended' in call
_ = Test4()

This is consistent with T? vs. Optional<T> sugaring elsewhere in the language, except this doesn't appear to affect @Direct:

struct Test1 { @Direct var direct: Foo }
struct Test2 { @Direct var direct: Foo = nil }
struct Test3 { @Direct var direct: Optional<Int> }
struct Test4 { @Direct var direct: Int? }

_ = Test1() // => Direct ExpressibleByNilLiteral used
_ = Test2() // => Direct wrappedValue used
_ = Test3() // => Direct ExpressibleByNilLiteral used
_ = Test4() // => Direct ExpressibleByNilLiteral used
1 Like

Good observations!

Even more simply it also applies to basic types with an empty init method added in an extension.

@propertyWrapper
struct Direct {
    let wrappedValue: Int

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

@propertyWrapper
struct Extended {
    let wrappedValue: Int

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

extension Extended {
    init() { self.wrappedValue = 0 }
}

// MARK: - Test

struct Test {
    @Direct var direct: Int
    @Extended var extended: Int
}

_ = Test() // error: Missing argument for parameter 'extended' in call
1 Like