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

问题描述 投票:0回答:3

我将通过一个例子来解释它。我们有一个具有

firstName
lastName
的强制协议,例如:

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

list
也有类似的流程。

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

struct Profiler {
    var profiles: [Profile]
}

第一期

符合

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

extension Profiler: ProfilerRepresentable { }

第二期

按照之前的模式,扩展

ProfilerRepresentable
无法按预期工作,并且会发出警告:

⚠️ 通过此函数的所有路径都会调用自身

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

顺便问一下,如何实现数组的目标?

swift swift-protocols protocol-oriented
3个回答
1
投票

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

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

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

1
投票

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

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

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

如果您需要 ProfilerRepresentable 的变量或数组,那么您应该问自己这些协议到底在做什么。哪些算法依赖于这些协议,以及 ProfileRepresentable 的哪些其他合理实现真正有意义?在许多情况下,ProfileRepresentable 应该只替换为简单的 Profile 结构,然后使用不同的

init
方法在不同的上下文中创建它。 (如果您的实际问题看起来很像您的示例,并且 Asperi 的答案不适合您,那么这就是我的建议。)

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

但是设计这个需要了解更具体的目标。没有普遍适用的通用答案。


查看您的评论,如果同一实体代表不同的事物,那么它们具有多种类型是没有问题的。结构就是值。可以同时拥有 Double 和 Float 类型,尽管每个 Float 也可以表示为 Double。因此,在您的情况下,您似乎只需要

Profile
PartialProfile
结构,以及一个可让您将其中一个转换为另一个的 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
。如果您的问题非常动态,那么您可能只需要更多动态类型。


0
投票

您可以通过将扩展的配置文件结果映射到所需的协议来实现它,以便编译器可以理解它:

extension ProfilerRepresentable where Self == Profiler {
    var profiles: [ProfileRepresentable] { self.profiles.map { $0 as ProfileRepresentable } }
}
© www.soinside.com 2019 - 2024. All rights reserved.