Swift - extensão KeyPath - impõe conformidade com o protocolo

Aug 27 2020

idealmente, gostaria de obter o nome da propriedade referenciada por um KeyPath. Mas isso parece não ser possível out-of-the-box em Swift.

Então, meu pensamento é que o KeyPath poderia fornecer essas informações com base na extensão de protocolo adicionada por um desenvolvedor. Então, eu gostaria de projetar uma API com um inicializador / função que aceite um KeyPath em conformidade com esse protocolo (que adiciona uma propriedade computada).

Até agora só consegui definir o protocolo e a conformidade condicional do protocolo. O código a seguir é compilado corretamente.

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"

Mas não consigo restringir a API para que o parâmetro de inicialização propertytenha que ser um KeyPath E deve estar em conformidade com um determinado protocolo.

Por exemplo, o seguinte resultaria em um erro de compilação, mas deve funcionar do meu entendimento (mas provavelmente perdi um detalhe importante;))

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"
    }
}

Quaisquer dicas são muito apreciadas!

Respostas

1 Sweeper Aug 27 2020 at 08:43

Você está entendendo mal a conformidade condicional. Parece que você quer fazer isso no 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 {
        ...
    }
}

Mas você não pode. Você está tentando especificar várias condições em conformidade com o mesmo protocolo. Veja aqui para obter mais informações sobre por que isso não está no Swift.

De alguma forma, uma parte do compilador pensa que a conformidade com KeyPathPropertyNameProvidingnão é condicional, então KeyPath<Model, T> & KeyPathPropertyNameProvidingé na verdade o mesmo que KeyPath<Model, T>, porque de alguma forma KeyPath<Model, T>já "está em conformidade" com KeyPathPropertyNameProvidingo compilador, só que a propriedade propertyNamesó estará disponível algumas vezes .

Se eu reescrever o inicializador desta forma ...

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

De alguma forma, isso faz com que o erro desapareça e produz um aviso:

Restrição de conformidade redundante 'KeyPathType': 'KeyPathPropertyNameProviding'

Jessy Aug 27 2020 at 11:29

Os caminhos das chaves podem ser hash, então eu recomendo um dicionário. É especialmente fácil colocá-lo junto com uma digitação forte se você for capaz de usar um 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]!
  }
}

Em seguida, use parênteses em vez do elenco que você demonstrou. Não há necessidade de protocolo até agora ...

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

Isso provavelmente ficará mais limpo devido ao suporte integrado algum dia. https://forums.swift.org/t/keypaths-and-codable/13945