无法为数组参数扩展约束协议

Posted

技术标签:

【中文标题】无法为数组参数扩展约束协议【英文标题】:Extending a constrained protocol for an array argument is not possible 【发布时间】:2020-07-18 13:04:17 【问题描述】:

我将通过一个例子来解释它。我们有一个具有firstNamelastName 的强制协议,例如:

protocol ProfileRepresentable 
    var firstName: String  get 
    var lastName: String  get 

我们要使用的类型有这两个,但是是可选的形式:

struct Profile 
    var firstName: String?
    var lastName: String?

所以在符合ProfileRepresentable之后,我们将扩展ProfileRepresentable并尝试返回值和nil状态的默认值:

extension Profile: ProfileRepresentable  
extension ProfileRepresentable where Self == Profile 
    var firstName: String  self.firstName ?? "NoFirstName" 
    var lastName: String  self.lastName ?? "NoLastName" 

到目前为止一切顺利

现在Profiles 的列表也有类似的流程。

protocol ProfilerRepresentable 
    var profiles: [ProfileRepresentable]  get 


struct Profiler 
    var profiles: [Profile]

第一期

符合ProfilerRepresentable自动按预期完成实现(因为Profile 已经符合ProfileRepresentable

extension Profiler: ProfilerRepresentable  

第二期

按照之前的模式,扩展ProfilerRepresentable 未按预期工作,并引发警告:

⚠️通过这个函数的所有路径都会调用自己

extension ProfilerRepresentable where Self == Profiler 
    var profiles: [ProfileRepresentable]  self.profiles 

顺便说一句,我怎样才能实现数组的目标?

【问题讨论】:

【参考方案1】:

这是可能的解决方案。使用 Xcode 12 / swift 5.3 测试

protocol ProfilerRepresentable 
    associatedtype T:ProfileRepresentable
    var profiles: [T]  get 


extension Profiler: ProfilerRepresentable  
struct Profiler 
    var profiles: [Profile]

【讨论】:

谢谢,但如果ProfilerRepresentable 进入另一种类型怎么办?我认为会有一个通用错误 @MojtabaHosseini 忘记使用协议作为您的属性类型。您是否见过有人将属性设置为 StringProtocol 用于 String 或 FloatingPoint 用于 Double? @LeoDabus 是的,我看到了,很遗憾。但对我来说,重点是找出我怎样才能做到这一点。不是我为什么要做到这一点。 (在这种情况下)。因为它将用于某些 API,并且可以在后面更改多种类型等。 @MojtabaHosseini,SwiftUI API 的核心是具有关联类型的协议,所以这是构建 API 的真正方式......只是不同。 再一次,我得到了only be used as a generic constraint because it has Self or associated type requirementsAll paths through this function will call itself。唯一可行的方法是为协议的属性和原始类型的属性使用不同的名称,例如:profiles vs profileList,我不喜欢这样。【参考方案2】:

[Profile] 不是[ProfileRepresentable] 的子类型。 (有关此问题的相关但不同的版本,请参阅Swift Generics & Upcasting。)当作为参数传递或分配给变量时,它可以通过编译器提供的复制步骤进行转换,但这对于那些非常常见用途。一般不适用。

你应该如何解决这个问题取决于你想用这种类型做什么。

如果您有一个依赖 ProfilerRepresentable 的算法,那么 Asperi 的解决方案是理想的,也是我推荐的。但是那样做将不允许您创建 ProfileRepresentable 类型的变量或将 ProfileRepresentable 放入数组中。

如果您需要 ProfilerRepresentable 的变量或数组,那么您应该问问自己这些协议到底在做什么。哪些算法依赖于这些协议,以及 ProfileRepresentable 的哪些其他合理实现真正有意义?在许多情况下,ProfileRepresentable 应该只替换为一个简单的 Profile 结构,然后在不同的上下文中使用不同的 init 方法来创建它。 (如果您的真正问题看起来很像您的示例,并且 Asperi 的答案对您不起作用,我建议您这样做。)

最终你可以创建类型橡皮擦(AnyProfile),但我建议先探索所有其他选项(特别是重新设计你的组合方式)。如果您的目标是擦除复杂或私有类型 (AnyPublisher),类型橡皮擦是完美的选择,但这通常不是人们伸手去拿它们时的意思。

但是设计它需要知道一个更具体的目标。没有普遍适用的普遍答案。


看看你的 cmets,如果它们代表不同的东西,那么对于同一个实体有多个类型是没有问题的。结构是值。可以同时使用 Double 和 Float 类型,即使每个 Float 也可以表示为 Double。因此,在您的情况下,您似乎只需要 ProfilePartialProfile 结构,以及一个可让您将一个转换为另一个的 init。

struct Profile 
    var firstName: String
    var lastName: String


struct PartialProfile 
    var firstName: String?
    var lastName: String?


extension Profile 
    init(_ partial: PartialProfile) 
        self.firstName = partial.firstName ?? "NoFirstName"
        self.lastName = partial.lastName ?? "NoLastName"
    


extension PartialProfile 
    init(_ profile: Profile) 
        self.firstName = profile.firstName
        self.lastName = profile.lastName
    

你可能有很多这样的东西,所以这可能会有点乏味。有很多方法可以解决这个问题,具体取决于您要解决的问题。 (我建议从编写具体代码开始,即使它会导致大量重复,然后看看如何删除这些重复。)

一个可能有用的工具是Partial<Wrapped>(受 TypeScript 启发),它可以创建任何非可选结构的“可选”版本:

@dynamicMemberLookup
struct Partial<Wrapped> 
    private var storage: [PartialKeyPath<Wrapped>: Any] = [:]

    subscript<T>(dynamicMember member: KeyPath<Wrapped, T>) -> T? 
        get  storage[member] as! T? 
        set  storage[member] = newValue 
    


struct Profile 
    var firstName: String
    var lastName: String
    var age: Int


var p = Partial<Profile>()
p.firstName = "Bob"
p.firstName    // "Bob"
p.age          // nil

还有一个类似的转换器:

extension Profile 
    init(_ partial: Partial<Profile>) 
        self.firstName = partial.firstName ?? "NoFirstName"
        self.lastName = partial.lastName ?? "NoLastName"
        self.age = partial.age ?? 0
    

现在继续你的数组问题,在这些之间切换只是一张地图。

var partials: [Partial<Profile>] = ...
let profiles = partials.map(Profile.init)

(当然,如果方便的话,您可以创建一个数组扩展,使其成为类似.asWrapped() 的方法。)

另一个方向在最简单的方法中略显乏味:

extension Partial where Wrapped == Profile 
    init(_ profile: Profile) 
        self.init()
        self.firstName = profile.firstName
        self.lastName = profile.lastName
        self.age = profile.age
    

如果有很多类型,可能值得让 Partial 稍微复杂一点,这样你就可以避免这种情况。这是一种方法,它允许 Partial 仍然是可变的(我希望这会很有价值),同时还允许它从包装的实例中被简单地映射。

@dynamicMemberLookup
struct Partial<Wrapped> 
    private var storage: [PartialKeyPath<Wrapped>: Any] = [:]
    private var wrapped: Wrapped?

    subscript<T>(dynamicMember member: KeyPath<Wrapped, T>) -> T? 
        get  storage[member] as! T? ?? wrapped?[keyPath: member] 
        set  storage[member] = newValue 
    


extension Partial 
    init(_ wrapped: Wrapped) 
        self.init()
        self.wrapped = wrapped
    

我不喜欢这个解决方案;它有一个奇怪的怪癖,partial.key = nil 无法清除值。但在我们得到KeyPathIterable 之前,我没有一个很好的解决方案。但是根据您的确切问题,您可以采取其他一些路线。当然,如果 Partial 不可变,事情会变得更简单。

关键是这里不需要协议。只是值和结构,并在需要时在它们之间进行转换。深入了解@dynamicMemberLookup。如果您的问题非常动态,那么您可能只需要更多动态类型。

【讨论】:

让我更专注于你的答案.....然后我会尝试更好地解释...... 好的,我已经多次阅读您的回答以确保我理解。目标是创建一个像Apollo 这样的服务,它根据查询创建类。但是由于类型可以以不同的属性出现在不同的地方,(like 可选项,但我们知道它们是否为 nil,所以它们不是可选项),我们不能使用具体类型(或我们最终会为同一个实体提供多种类型)。

以上是关于无法为数组参数扩展约束协议的主要内容,如果未能解决你的问题,请参考以下文章

具有泛型的Swift函数,其中约束是自身符合的协议

约束集问题,无法找出问题

dubbo协议约束

为啥我会收到错误“协议……只能用作通用约束,因为它具有自身或关联的类型要求”?

在 Swift 中声明具有类型约束的属性

TypeScript:相当于 C# 的用于扩展类的通用类型约束?