SwiftUI Picker报错:Type 'any Target'无法遵循Hashable的解决方法
我正在开发一个SwiftUI项目,定义了遵循Identifiable和Hashable的Target协议,还有两个实现该协议的结构体Silhouette和IPSC。想在ContentView里用Picker让用户选择目标,但代码无法编译。
我的代码:
import SwiftUI protocol Target: Identifiable, Hashable { var id: UUID { get } var name: String { get } var description: String { get } } struct Silhouette: Target { let id = UUID() var name: String = "Silhouette" var description: String = "A normal target" } struct IPSC: Target { let id = UUID() var name: String = "IPSC" var description: String = "A special target" } struct ContentView: View { private let targets: [any Target] = [Silhouette(), IPSC()] @State private var selectedTarget: any Target = Silhouette() var body: some View { Picker("Targets", selection: $selectedTarget) { ForEach(targets, id: \.id) { target in Text(target.name) } } } }
报错信息:
Type 'any Target' cannot conform to 'Hashable'
明明Silhouette和IPSC都遵循了Hashable,为什么编译器会报错?怎么在保留协议架构的前提下解决这个问题?
报错原因
虽然Target协议继承了Hashable,且具体结构体都实现了该协议,但**any Target是存在类型(existential type)**,它本身并不会自动遵循Hashable。Swift的存在类型不会自动继承协议的一致性,因为不同的Target实现可能有不同的哈希逻辑,编译器无法确定如何对any Target实例执行哈希和相等性判断。
解决方案
方案1:为any Target手动实现Hashable一致性
通过扩展Target协议,提供默认的哈希和相等判断逻辑,基于协议定义的属性统一处理:
extension Target { static func == (lhs: any Target, rhs: any Target) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name && lhs.description == rhs.description } func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(name) hasher.combine(description) } }
添加这段扩展后,any Target就能满足Hashable的要求,原有Picker代码可正常编译运行。
方案2:用枚举封装目标类型
如果你的目标类型是固定的几个,用枚举实现Target协议是更简洁的选择——枚举本身天然遵循Hashable(只要关联值都符合Hashable):
enum TargetType: Target, CaseIterable { case silhouette, ipsc var id: UUID { switch self { case .silhouette: return UUID(uuidString: "00000000-0000-0000-0000-000000000001")! case .ipsc: return UUID(uuidString: "00000000-0000-0000-0000-000000000002")! } } var name: String { switch self { case .silhouette: return "Silhouette" case .ipsc: return "IPSC" } } var description: String { switch self { case .silhouette: return "A normal target" case .ipsc: return "A special target" } } } // 修改后的ContentView struct ContentView: View { @State private var selectedTarget: TargetType = .silhouette var body: some View { Picker("Targets", selection: $selectedTarget) { ForEach(TargetType.allCases, id: \.id) { target in Text(target.name) } } } }
这种方式规避了存在类型的限制,适合目标类型数量固定的场景。
方案3:使用类型擦除包装器
创建一个类型擦除的AnyTarget结构体,持有具体的Target实例并实现Hashable,保留协议抽象的同时解决存在类型的问题:
struct AnyTarget: Target, Hashable { private let base: any Target var id: UUID { base.id } var name: String { base.name } var description: String { base.description } init(_ base: any Target) { self.base = base } static func == (lhs: AnyTarget, rhs: AnyTarget) -> Bool { lhs.base.id == rhs.base.id && lhs.base.name == rhs.base.name && lhs.base.description == rhs.base.description } func hash(into hasher: inout Hasher) { hasher.combine(base.id) hasher.combine(base.name) hasher.combine(base.description) } } // 修改后的ContentView struct ContentView: View { private let targets: [AnyTarget] = [AnyTarget(Silhouette()), AnyTarget(IPSC())] @State private var selectedTarget: AnyTarget = AnyTarget(Silhouette()) var body: some View { Picker("Targets", selection: $selectedTarget) { ForEach(targets, id: \.id) { target in Text(target.name) } } } }
内容的提问来源于stack exchange,提问作者MohAliBou

