Как получить динамическую c отправку для ограниченных дженериков в инициализаторе - PullRequest
1 голос
/ 21 июня 2020

Код ниже работает нормально:

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])
    }
    
}

Вызов:

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

приводит к тому, что начальное значение равно 0.

Проблема возникает, когда Я пытаюсь расширить это для поддержки RawRepresentable типов:

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])
    }
}

Когда я вызываю это:

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)

код вылетает, потому что вместо этого вызывается реализация registerDefaults в UserDefaultsVariable<T> специальной реализации UserDefaultsVariable<T:RawRepresentable>.

Выполнение вызова из вне инициализатора, например,

enumedDefault.registerDefaultValue(initialValue: .left)

вызывает правильную реализацию !? Итак, похоже, что отправка обычно динамический c, но не внутри инициализатора? или, возможно, весь класс?

Любая помощь будет очень принята.

Решение

Как указывает @matt , Я ошибочно ожидаю, что Swift вызовет ограниченную версию функции с помощью некоторой формы полиморфизма, но компилятор разрешает вызов во время компиляции и не пытается найти «наиболее конкретную c реализацию» ..

@ Asperi представляет собой работоспособное решение, но с тем недостатком, что переменная DefaultsVariable может быть инициализирована без начального значения, чего я пытался избежать.

В конечном итоге я ввел полиморфизм, просто создав подклассы 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

Это прекрасно компилируется и вызывает registerDefaultValue в подклассе из инициализатора в суперклассе.

Я очень благодарен за помощь.

Ответы [ 2 ]

0 голосов
/ 21 июня 2020

Здесь возможен подход.

Идея состоит в том, чтобы сделать инициализатор удобным в расширении, чтобы универсальное специализированное расширение перекрывало стандартное расширение.

Протестировано и работает с 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])
    }
}
0 голосов
/ 21 июня 2020

Я думаю, что концептуальная проблема, с которой вы столкнулись, состоит в том, что на самом деле в этой истории нет «динамической c отправки». Dynami c dispatch - это механизм времени выполнения, включающий полиморфизм, которого ваш код не содержит. Общий c полностью разрешается во время компиляции: то, что произойдет, явно записано прямо в код.

Чтобы понять, что я имею в виду, просто сделайте следующее: измените все свои UserDefaultsVariable<T>(...) на UserDefaultsVariable<T>.init(...) на дайте себе что-нибудь, на что можно щелкнуть, и просто начните щелкать термины, удерживая клавиши Control-Command. Вы увидите, где мы переходим к тому, как компилятор маршрутизирует каждый вызов.

enumedDefault это типизировано как UserDefaultsVariable<Direction>, явно . Вот как вы набрали. Итак, мы (королевский компилятор «мы») знаем, что вызывать registerDefaultValue. Нет никакой "рассылки". Но нет (и не может быть) какого-либо специального обозначенного init, определенного для расширения RawRepresentable, а self просто self в инициализаторе. Таким образом, инициализатор не будет волшебным образом вызывать другой registerDefaultValue на основе какого-то теста времени выполнения, чтобы определить, соответствует ли общий c параметризованный тип RawRepresentable.

...