Search code examples
swiftgenericsinitializerdynamic-dispatch

How to get dynamic dispatch for constrained generics in initializer


The code below works just fine:

protocol VariableType {
    
    associatedtype T
    
    var value : T { get }
 
}

class UserDefaultsVariable<T> : VariableType {
   
    let storageKey : String
    
    var value : T {
        
        get {
            return UserDefaults.standard.object(forKey: storageKey) as! T
        }
        
        set(value) {
            UserDefaults.standard.set(value, forKey: storageKey)
        }
    }
    
    init( storageKey : String, initialValue : T  ) {
        self.storageKey = storageKey
        // I want dynamic dispatch here so that if T: RawRepresentable then the
        // function in the extension is called
        self.registerDefaultValue(initialValue: initialValue)
    }
    
    func registerDefaultValue( initialValue : T ) {
        debugPrint("this is called both times!")
        UserDefaults.standard.register(defaults: [storageKey : initialValue])
    }
    
}

Calling:

let simpleDefault = UserDefaultsVariable<Int>(storageKey: "simple", initialValue: 0)
let initialValue = simpleDefault.value

results in the initial value being 0 as expected.

The problem arises when I try to extend this to support RawRepresentable types:

extension UserDefaultsVariable where T: RawRepresentable {
    
    var value: T {
        get {
            let rawValue = UserDefaults.standard.object(forKey: storageKey) as! T.RawValue
            return T(rawValue: rawValue)!
        }
        set {
            UserDefaults.standard.set(newValue.rawValue, forKey: storageKey)
        }
    }
    
    func registerDefaultValue( initialValue : T ) {
        debugPrint("this is never called!")
        UserDefaults.standard.register(defaults: [storageKey:initialValue.rawValue])
    }
}

When I call this:

let simpleDefault = UserDefaultsVariable<Int>(storageKey: "simple", initialValue: 0)
let initialValue = simpleDefault.value

enum Direction : Int {
    case left = 0
    case right = 1
}

let enumedDefault = UserDefaultsVariable<Direction>(storageKey: "enumed", initialValue: .left)

the code crashes because the registerDefaults implementation in the UserDefaultsVariable<T> is called instead of the specialized UserDefaultsVariable<T:RawRepresentable> implementation.

Making the call from outside the initializer, e.g.

enumedDefault.registerDefaultValue(initialValue: .left)

calls the correct implementation!? So it looks like the dispatch is usually dynamic, but not inside of an initializer? or possibly the entire class?

Any help would be very much appreciated.

Solution

As @matt points out, I wrongly expect Swift to call the constrained version of the function via some form of polymorphism, but the compiler resolves the call at compile time and makes no attempt at finding "the most specific implementation"..

@Asperi presents a workable solution, but with the disadvantage that the DefaultsVariable can be initialized without an initial value, which is something I was trying to avoid.

What I ended up doing was to introduce polymorphism by simply subclassing UserDefaultsVariable:

class RawRepresentableUserDefaultsVariable<T: RawRepresentable> : UserDefaultsVariable<T>{
    
    override var value: T {
        get {
            let rawValue = UserDefaults.standard.object(forKey: storageKey) as! T.RawValue
            return T(rawValue: rawValue)!
        }
        set {
            UserDefaults.standard.set(newValue.rawValue, forKey: storageKey)
        }
    }
    
    override func registerDefaultValue( initialValue : T ) {
        UserDefaults.standard.register(defaults: [storageKey:initialValue.rawValue])
    }
}

let enumedDefault = RawRepresentableUserDefaultsVariable<Direction>(storageKey: "enumed", initialValue: .left)

let initialEnumValue = enumedDefault.value

This compiles fine and calls the registerDefaultValue in the subclass from the initializer in the superclass.

I really appreciate the help.


Solution

  • Here is possible approach.

    The idea is to make initializer convenience in extension, so generics specialised extension overlaps default one.

    Tested & works with Xcode 11.4 / swift 5.2

    class UserDefaultsVariable<T> : VariableType {
    
        let storageKey : String
    
        init(storageKey: String) {
            self.storageKey = storageKey
        }
    
        var value : T {
    
            get {
                return UserDefaults.standard.object(forKey: storageKey) as! T
            }
    
            set(value) {
                UserDefaults.standard.set(value, forKey: storageKey)
            }
        }
    
        func registerDefaultValue( initialValue : T ) {
            UserDefaults.standard.register(defaults: [storageKey : initialValue])
        }
    
    }
    
    extension UserDefaultsVariable {
    
        // this initializer uses default T
        convenience init(storageKey : String, initialValue : T) {
    
            self.init(storageKey: storageKey)
            self.registerDefaultValue(initialValue: initialValue)
        }
    }
    
    extension UserDefaultsVariable where T: RawRepresentable {
    
        // this initializer uses specialised T
        convenience init(storageKey : String, initialValue : T) {
    
            self.init(storageKey: storageKey)
            self.registerDefaultValue(initialValue: initialValue)
        }
    
        var value: T {
            get {
                let rawValue = UserDefaults.standard.object(forKey: storageKey) as! T.RawValue
                return T(rawValue: rawValue)!
            }
            set {
                UserDefaults.standard.set(newValue.rawValue, forKey: storageKey)
            }
        }
    
        func registerDefaultValue( initialValue : T ) {
            UserDefaults.standard.register(defaults: [storageKey:initialValue.rawValue])
        }
    }