swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.28k stars 10.33k forks source link

[SR-11674] Unable to write property wrapper for weak protocol value #54083

Open regexident opened 4 years ago

regexident commented 4 years ago
Previous ID SR-11674
Radar None
Original Reporter @regexident
Type Bug
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 0 | |Component/s | Compiler | |Labels | Bug | |Assignee | None | |Priority | Medium | md5: d237cea5fceb17d115a2a19985992a57

Issue Description:

Let's assume one wants to wrap an optional value in a `@propertyWrapper`, so that on every read access a message is logged in case the value is `nil`.

Easy peasy:

@propertyWrapper
public struct CheckedOptional<Value> {
    private var value: Value?

    public init() {
        self.value = nil
    }

    public var wrappedValue: Value? {
        get {
            if self.value == nil {
                print("Expected value, found nil")
            }
            return self.value
        }
        set {
            self.value = newValue
        }
    }
}

Now let's assume one wanted to change

public struct CheckedOptional<Value> { … }

into

@propertyWrapper
public struct CheckedOptional<Value> {
    private weak var value: Value?

    public init() {
        self.value = nil
    }

    public var wrappedValue: Value? {
        get {
            if self.value == nil {
                print("Expected value, found nil")
            }
            return self.value
        }
        set {
            self.value = newValue
        }
    }
}

which fails due to:

error: 'weak' must not be applied to non-class-bound 'Value'; consider adding a protocol conformance that has a class bound

Now changing

struct CheckedWeakOptional<Value {
    private weak var value: Value?
    // …
}

to

struct CheckedWeakOptional<Value: AnyObject> {
    private weak var value: Value?
    // …
}

leads to this error:

error: property type 'FooDelegate?' does not match that of the 'wrappedValue' property of its wrapper type 'CheckedWeakOptional'

Here is the full code

protocol FooDelegate: AnyObject {
    func foo(_ foo: Foo, bar: Bool)
}

@propertyWrapper
struct CheckedWeakOptional<Value: AnyObject> {
    private weak var value: Value?

    init() {
        self.value = nil
    }

    var wrappedValue: Value? {
        get {
            if self.value == nil {
                print("Expected value, found nil")
            }

            return self.value
        }
        set { self.value = newValue }
    }
}

class Foo {
    @CheckedWeakOptional var delegate: FooDelegate?

    func bar() {
        self.delegate?.foo(self, bar: true)
    }
}

class SomeFooDelegate: FooDelegate {
    func foo(_ foo: Foo, bar: Bool) {
        print("Received foo: \(bar)")
    }
}

let foo = Foo()
let someDelegate = SomeFooDelegate()

foo.bar()

foo.delegate = someDelegate

foo.bar()

Note that the whole thing does compile once one changes

@CheckedWeakOptional var delegate: FooDelegate?

to

@CheckedWeakOptional var delegate: SomeFooDelegate?

which unfortunately defeats the whole point of having a protocol in the first place and as such is not a feasible solution.

regexident commented 4 years ago

This is a working workaround with its own complications:

protocol FooDelegate {
    func foo(_ foo: Foo, bar: Bool)
}

@propertyWrapper
struct CheckedWeakOptional<Value> {
    weak var value: AnyObject? = nil

    var wrappedValue: Value? {
        get {
            if self.value == nil {
                print("Expected value, found nil")
            }

            return self.value as? Value
        }
        set {
            self.value = newValue as AnyObject
        }
    }
}

class Foo {
    @CheckedWeakOptional var delegate: FooDelegate?

    func bar() {
        print(type(of: self), #function)
        self.delegate?.foo(self, bar: true)
    }
}

struct StructFooDelegate: FooDelegate {
    func foo(_ foo: Foo, bar: Bool) {
        print(type(of: self), #function)
    }
}

class ClassFooDelegate: FooDelegate {
    func foo(_ foo: Foo, bar: Bool) {
        print(type(of: self), #function)
    }
}

let foo = Foo()
let classDelegate = ClassFooDelegate()
let structDelegate = StructFooDelegate()

foo.bar()
// Output:
// Foo bar()
// Expected value, found nil

print()

foo.delegate = classDelegate
foo.bar()
// Output:
// Foo bar()
// ClassFooDelegate foo(_:bar:)

print()

foo.delegate = structDelegate
foo.bar()
// Output:
// Foo bar()
// Expected value, found nil
belkadan commented 4 years ago

I wonder if the right answer is going to be a built-in constraint ("layout constraint" in Swift compiler parlance) that's weaker than AnyObject: "reference-counted and class-bound in some way". We don't get the "single retainable pointer" optimization, but it would allow arbitrary weak references without having to cast in and out. Meanwhile, that's another possible workaround:

@propertyWrapper
public struct CheckedOptional<Value> {
    private weak var value: AnyObject? // NEW

    public init() {
        self.value = nil
    }

    public var wrappedValue: Value? {
        get {
            if self.value == nil {
                print("Expected value, found nil")
            }
            return self.value as! Value? // NEW
        }
        set {
            self.value = newValue
        }
    }
}

cc @slavapestov, @jckarter