Swift-KeyPath扩展-强制遵守协议

问题描述

理想情况下,我想获取KeyPath引用的属性名称。但这似乎不可能在Swift中立即可用。

所以我的想法是KeyPath可以基于开发人员添加的协议扩展来提供此信息。然后,我想设计一个带有初始化器/函数的API,该初始化器/函数接受符合该协议的KeyPath(添加计算属性)。

到目前为止,我只能定义协议和协议的条件一致性。以下代码可以正常编译。

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"

但是我无法限制API,因此初始化参数property必须是KeyPath并且必须符合特定协议。

例如,以下内容将导致编译错误,但根据我的理解应该可以运行(但可能我错过了一个关键细节;))

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

任何提示都将受到高度赞赏!

解决方法

您误解了条件一致性。您似乎将来想这样做:

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 {
        ...
    }
}

但是你不能。您正在尝试指定多个条件以符合同一协议。请参阅here,详细了解为何Swift中没有此功能。

某种程度上,编译器的一部分认为对KeyPathPropertyNameProviding的符合不是条件性的,因此KeyPath<Model,T> & KeyPathPropertyNameProviding实际上与KeyPath<Model,T>相同,因为某种程度上KeyPath<Model,T>已经“符合KeyPathPropertyNameProviding就编译器而言,只是属性propertyName仅在有时可用。

如果我用这种方式重写初始化程序...

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

某种方式使错误消失并产生警告:

冗余一致性约束'KeyPathType':'KeyPathPropertyNameProviding'

,

键路径是可哈希的,因此我建议使用字典。如果您能够使用CodingKey类型,则将其与强类型结合在一起特别容易。

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

然后使用括号代替您演示的演员表。到目前为止,无需协议……

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

由于有内置的支持,这可能会更加干净。 https://forums.swift.org/t/keypaths-and-codable/13945