Swift - extensión KeyPath - hacer cumplir el protocolo

Aug 27 2020

idealmente, me gustaría obtener el nombre de la propiedad a la que hace referencia un KeyPath. Pero esto parece no ser posible de inmediato en Swift.

Entonces, mi pensamiento es que KeyPath podría proporcionar esta información en función de la extensión de protocolo agregada por un desarrollador. Luego me gustaría diseñar una API con un inicializador / función que acepte un KeyPath conforme a ese protocolo (que agrega una propiedad calculada).

Hasta ahora solo pude definir el protocolo y la conformidad condicional del protocolo. El siguiente código se compila bien.

protocol KeyPathPropertyNameProviding {
    var propertyName: String {get}
}

struct User {
    var name: String
    var age: Int
}

struct Person  {
    var name: String
    var age: Int
}

extension KeyPath: KeyPathPropertyNameProviding where Root == Person {
    var propertyName: String {
        switch self {
            case \Person.name: return "name"
            case \Person.age: return "age"
            default: return ""
        }
    }
}

struct PropertyWrapper<Model> {
    var propertyName: String = ""
    init<T>(property: KeyPath<Model, T>) {
        if let property = property as? KeyPathPropertyNameProviding {
            self.propertyName = property.propertyName
        }
    }
}

let userAge = \User.age as? KeyPathPropertyNameProviding
print(userAge?.propertyName) // "nil"
let personAge = \Person.age as? KeyPathPropertyNameProviding
print(personAge?.propertyName) // "age"

let wrapper = PropertyWrapper<Person>(property: \.age)
print(wrapper.propertyName) // "age"

Pero no puedo restringir la API, por lo que el parámetro de inicialización propertydebe ser un KeyPath Y debe ajustarse a un protocolo determinado.

Por ejemplo, lo siguiente resultaría en un error de compilación, pero debería funcionar desde mi entendimiento (pero probablemente me pierdo un detalle clave;))

struct PropertyWrapper<Model> {
    var propertyName: String = ""
    init<T>(property: KeyPath<Model, T> & KeyPathPropertyNameProviding) {
        self.propertyName = property.propertyName // compilation error "Property 'propertyName' requires the types 'Model' and 'Person' be equivalent"
    }
}

¡Cualquier consejo es muy apreciado!

Respuestas

1 Sweeper Aug 27 2020 at 08:43

Está malinterpretando la conformidad condicional. Parece que quieres hacer esto en el futuro:

extension KeyPath: KeyPathPropertyNameProviding where Root == Person {
    var propertyName: String {
        switch self {
            case \Person.name: return "name"
            case \Person.age: return "age"
            default: return ""
        }
    }
}

extension KeyPath: KeyPathPropertyNameProviding where Root == User {
    var propertyName: String {
        ...
    }
}

extension KeyPath: KeyPathPropertyNameProviding where Root == AnotherType {
    var propertyName: String {
        ...
    }
}

Pero no puedes. Está intentando especificar varias condiciones para cumplir con el mismo protocolo. Consulte aquí para obtener más información sobre por qué esto no está en Swift.

De alguna manera, una parte del compilador piensa que la conformidad con KeyPathPropertyNameProvidingno es condicional, por KeyPath<Model, T> & KeyPathPropertyNameProvidinglo que en realidad es lo mismo que KeyPath<Model, T>, porque de alguna manera KeyPath<Model, T>ya se "ajusta" en lo KeyPathPropertyNameProvidingque respecta al compilador, es solo que la propiedad propertyNamesolo estará disponible algunas veces .

Si reescribo el inicializador de esta manera ...

init<T, KeyPathType: KeyPath<Model, T> & KeyPathPropertyNameProviding>(property: KeyPathType) {
    self.propertyName = property.propertyName
}

Esto de alguna manera hace que el error desaparezca y produce una advertencia:

Restricción de conformidad redundante 'KeyPathType': 'KeyPathPropertyNamePro provide'

Jessy Aug 27 2020 at 11:29

Las rutas de las claves se pueden utilizar con hash, así que recomiendo un diccionario. Es especialmente fácil juntarlo con una escritura fuerte si puede usar un CodingKeytipo.

struct Person: Codable  {
  var name: String
  var age: Int

  enum CodingKey: Swift.CodingKey {
    case name
    case age
  }
}

extension PartialKeyPath where Root == Person {
  var label: String {
    [ \Root.name: Root.CodingKey.name,
      \Root.age: .age
    ].mapValues(\.stringValue)[self]!
  }
}

Luego use paréntesis en lugar del elenco que demostró. No es necesario un protocolo hasta ahora ...

(\Person.name).label // "name"
(\Person.age).label // "age"

Probablemente todo esto estará más limpio debido al soporte integrado algún día. https://forums.swift.org/t/keypaths-and-codable/13945