Search code examples
swiftgenericskeypaths

Swift Keypath Generic and Subclassing


I'm working on a class where a superclass and its subclasses have varying properties, but all of the same type that need identical processing before being assigned. I've come up with a quite contrived and simplified example of what I'm trying to do with key paths, possibly using generics, including a non optimal, yet working variation.

class OriginalClass {
    // this will only allow for properties that exist available on the base `OriginalClass` (and that makes sense)
    func updateAProperty(to value: Int, keyPath: ReferenceWritableKeyPath<OriginalClass, Int>) {
        // lots of custom, but common logic
        self[keyPath: keyPath] = value
    }

    // this *works*, but I don't like the cast I have to do on the first line, and the call site requires explicit
    // keypaths (including the type)
    func updateAPropertyTwo<GenClass>(to value: Int, keyPath: ReferenceWritableKeyPath<GenClass, Int>) {
        guard let self = self as? GenClass else { return }
        self[keyPath: keyPath] = value
    }

    // ideally, i want to do something like this. Basically, the compiler should (aka i WANT it to) be able to tell that
    // im working off a subclass of OriginalClass and provide the keypaths available to the subclass in addition to those
    // on the base, superclass.
//  func idealNonworking(to value: Int, keyPath: ReferenceWritableKeyPath<*AutomaticallyReplacedWithWhateverSubclass*, Int>) {
//      // lots of custom, but common logic
//      self[keyPath: keyPath] = value
//  }

    // this complains that `Same-type requirement makes generic parameter 'GenClass' non-generic`, but afaik, if it DID
    // work it SHOULD include the subclass properties (but it doesn't, to be clear)
//  func alternativeIdealYetNonworking<GenClass>(to value: Int, keyPath: ReferenceWritableKeyPath<GenClass, Int>) where GenClass == Self {
//      // lots of custom, but common logic
//      self[keyPath: keyPath] = value
//  }
}


class SecondClass: OriginalClass {
    var subclassValue = 0

    func nonWorkingExample() {
//      updateAProperty(to: subclassValue + 1, keyPath: \.subclassValue)
    }

    func subOptimalWorkingExample() {
        updateAPropertyTwo(to: subclassValue + 1, keyPath: \SecondClass.subclassValue)
//      updateAPropertyTwo(to: subclassValue + 1, keyPath: \Self.subclassValue) // runs into a runtime demangling error
//      updateAPropertyTwo(to: subclassValue + 1, keyPath: \.subclassValue)
    }

//  func optimalYetNonworkingExample() {
//      idealNonworking(to subclassValue + 1, keyPath: \.subclassValue)
//  }
}

let test = SecondClass()
print(test.subclassValue)
test.subOptimalWorkingExample()
print(test.subclassValue)

Now, I know WHY the first one won't work (the key path type is defined by the properties available on OriginalClass), but I'm not sure why the last one wouldn't work. Of course, that's less important than if someone knows how to do this


Solution

  • Self is allowed in protocol extensions, so I wrote just that:

    protocol P {
        // put whatever methods and properties from OriginalClass the "lots of custom,
        // but common logic" need here...
    }
    class OriginalClass : P {}
    
    extension P {
        func updateAProperty(to value: Int, keyPath: ReferenceWritableKeyPath<Self, Int>) {
          // lots of custom, but common logic
            self[keyPath: keyPath] = value
        }
    }
    

    And usage:

    class SecondClass: OriginalClass {
        var subclassValue = 0
    
        func workingExample() {
          updateAProperty(to: subclassValue + 1, keyPath: \.subclassValue)
        }
    }
    
    let test = SecondClass()
    print(test.subclassValue)
    test.workingExample()
    print(test.subclassValue)