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

时间:2020-07-18 13:04:17

标签: swift swift-protocols protocol-oriented

我将通过一个例子进行解释。我们有一个强制使用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" }
}

到目前为止一切都很好

现在,Profile列表具有类似的流程。

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

我怎样才能实现数组的目标?

2 个答案:

答案 0 :(得分:1)

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

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

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

答案 1 :(得分:1)

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

您应如何解决此问题取决于您要对这种类型进行精确的操作。

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

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

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

但是要设计此功能,需要知道更具体的目标。没有普遍适用的普遍答案。


查看您的评论,如果同一实体代表不同的事物,则具有多个类型是没有问题的。结构是价值。可以同时使用Double和Float类型,即使每个Float也可以表示为Double。因此,在您的情况下,您似乎只需要ProfilePartialProfile结构,以及一个使您可以将一个转换为另一个的初始化。

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)

(当然,如果方便,您可以创建一个Array扩展名,使之成为类似.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。如果您的问题非常动态,那么您可能只需要更多动态类型。