swiftlang / swift

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

[SR-12504] Property Wrapper constrained extension bug #54946

Open 05109ee7-7cd9-4cd4-92d0-698e676fc6af opened 4 years ago

05109ee7-7cd9-4cd4-92d0-698e676fc6af commented 4 years ago
Previous ID SR-12504
Radar rdar://problem/57435182
Original Reporter @an0
Type Bug
Additional Detail from JIRA | | | |------------------|-----------------| |Votes | 1 | |Component/s | Compiler | |Labels | Bug, PropertyWrappers | |Assignee | None | |Priority | Medium | md5: afdba86880c3e9b8dc9febeb4a572395

Issue Description:

If I use a @propertyWrapper struct directly, its constrained extension works. However, when used as property wrapper, getter and setter of `wrappedValue` in the extension are not called, instead the general ones are used.

import Foundation

enum E: Int {
    case e1
    case e2
    case e3
}

let defaults = Foundation.UserDefaults.standard

@propertyWrapper
struct DefaultUserDefaults<T> {

    let key: String
    let defaultValue: T

    init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            print("get T for \(key)")
            return defaults.object(forKey: key) as? T ?? defaultValue
        }
        set {
            print("set T for \(key)")
            defaults.set(newValue, forKey: key)
        }
    }

}

extension DefaultUserDefaults where T: RawRepresentable {

    var wrappedValue: T {
        get {
            print("get RawRepresentable for \(key)")
            if let rawValue = defaults.object(forKey: key) as? T.RawValue, let value = T(rawValue: rawValue) {
                return value
            } else {
                return defaultValue
            }
        }
        set {
            print("set RawRepresentable for \(key)")
            defaults.set(newValue.rawValue, forKey: key)
        }
    }

}

var a = DefaultUserDefaults(key: "a", defaultValue: 0)
var e = DefaultUserDefaults(key: "e", defaultValue: E.e1)
print(a.wrappedValue)
print(e.wrappedValue)
a.wrappedValue = 1
e.wrappedValue = .e2
print(a.wrappedValue)
print(e.wrappedValue)

print()

class Prefs: NSObject {
    @DefaultUserDefaults(key: "a", defaultValue: 0) static var a: Int
    @DefaultUserDefaults(key: "e", defaultValue: .e1) static var e: E
}

print(Prefs.a)
// Should call RawRepresentable version but not.
print(Prefs.e)
Prefs.a = 1
// Should call RawRepresentable version but not, causes crash.
Prefs.e = .e3
print(Prefs.a)
// Should call RawRepresentable version but not.
print(Prefs.e)
beccadax commented 4 years ago

@swift-ci create

hborla commented 4 years ago

Aside from the bug, providing the special behavior for RawRepresentable-conforming types through an overload that's resolved statically can still have unexpected behavior. For example, if you were to use @DefaultUserDefaults in a generic context, e.g.

struct FeaturePlugin<UserDefaultValue> {
  @DefaultUserDefaults
  var value: UserDefaultValue

  init(key: String, value: UserDefaultValue) {
    _value = DefaultUserDefaults(key: key, defaultValue: value)
  }
}

then the compiler will never choose the specific RawRepresentable overload for wrappedValue. One way to fix this (and a workaround for the bug) is to provide the behavior that you want through your own protocol, for example:

protocol UserDefaultsWritable {
  static func getValue(for key: String) -> Self?
  static func setValue(_ newValue: Self, for key: String)
}

And your property wrapper can look something like this:

@propertyWrapper
struct DefaultUserDefaults<T: UserDefaultsWritable> {
  let key: String
  let defaultValue: T

  var wrappedValue: T {
    get { return T.getValue(for: key) ?? defaultValue }
    set { T.setValue(newValue, for: key) }
  }
}

Then, you can conditionally provide a default implementation for UserDefaultsWritable for types that conform to RawRepresentable (and a different default implementation for types that don't) and get the expected behavior. Note that with this approach, you'll have to explicitly specify conformance to UserDefaultsWritable for any type that you want to use with DefaultUserDefaults.