Swift - extensão KeyPath - impõe conformidade com o protocolo
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
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'
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