我将通过一个例子来解释它。我们有一个具有
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 }
}
顺便问一下,如何实现数组的目标?
这是可能的解决方案。使用 Xcode 12 / swift 5.3 进行测试
protocol ProfilerRepresentable {
associatedtype T:ProfileRepresentable
var profiles: [T] { get }
}
extension Profiler: ProfilerRepresentable { }
struct Profiler {
var profiles: [Profile]
}
[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
。如果您的问题非常动态,那么您可能只需要更多动态类型。
您可以通过将扩展的配置文件结果映射到所需的协议来实现它,以便编译器可以理解它:
extension ProfilerRepresentable where Self == Profiler {
var profiles: [ProfileRepresentable] { self.profiles.map { $0 as ProfileRepresentable } }
}