带关联值的枚举多键路径动态成员查找编译错误排查
问题描述
为带关联值的枚举实现动态成员查找时,添加多个键路径下标会引发编译错误。具体场景:
- 定义枚举
Media,包含movie(Movie)和book(Book)两个关联值 Movie遵循Filmed协议,Book遵循Written协议,两者均继承自Entertainment协议- 仅实现针对
Entertainment的动态成员下标时,可正常访问isbn、title等属性 - 添加针对
Filmed的可选动态成员下标后,访问media.title、media.sales出现Ambiguous use of 'subscript(dynamicMember:)'编译错误,移除第二个下标则错误消失
初始可正常运行的代码
@dynamicMemberLookup enum Media { case movie(Movie) case book(Book) subscript<T>(dynamicMember keyPath: KeyPath<Entertainment, T>) -> T { switch self { case .movie(let movie): return movie[keyPath: keyPath] case .book(let book): return book[keyPath: keyPath] } } } protocol Entertainment { var isbn: Int { get } var title: String { get } var sales: Int { get set } } protocol Written: Entertainment { var author: String { get } } protocol Filmed: Entertainment { var actors: [String] { get set } } struct Movie: Filmed { var isbn: Int var title: String var sales: Int var actors: [String] } struct Book: Written { var isbn: Int var title: String var sales: Int var author: String }
添加的引发错误的下标代码
subscript<T>(dynamicMember keyPath: KeyPath<Filmed, T>) -> T? { switch self { case .movie(let movie): return movie[keyPath: keyPath] default: return nil } }
错误场景代码
let media: Media = .movie(Movie(isbn: 1011, title: "Top Gun", sales: 1000, actors: ["Tom Cruise", "Miles Teller"])) print(media.title) // 编译错误: Ambiguous use of 'subscript(dynamicMember:)' print(media.sales) // 编译错误: Ambiguous use of 'subscript(dynamicMember:)' print(media.actors) // 无错误,可正常执行
问题原因
Filmed协议继承自Entertainment,因此所有属于Entertainment的属性(如title、sales)同时也是Filmed的属性。当访问media.title时,编译器可以生成两种合法的KeyPath:
KeyPath<Entertainment, String>,匹配返回非可选值的第一个下标KeyPath<Filmed, String>,匹配返回可选值的第二个下标
由于Swift重载解析不会优先根据返回类型选择,编译器无法确定调用哪个下标,因此抛出歧义错误。
而media.actors无问题,是因为actors是Filmed特有的属性,只有第二个下标能匹配,编译器可明确选择。
解决方案
方法一:拆分协议,分离公共与特有属性
将公共属性和各类型特有属性拆分到不同协议中,让动态成员下标对应不同的协议根,避免KeyPath匹配冲突:
// 公共属性协议 protocol Entertainment { var isbn: Int { get } var title: String { get } var sales: Int { get set } } // 影视特有属性协议 protocol FilmedSpecific { var actors: [String] { get set } } // 书籍特有属性协议 protocol WrittenSpecific { var author: String { get } } // 组合协议 protocol Filmed: Entertainment, FilmedSpecific {} protocol Written: Entertainment, WrittenSpecific {} @dynamicMemberLookup enum Media { case movie(Movie) case book(Book) // 处理公共属性 subscript<T>(dynamicMember keyPath: KeyPath<Entertainment, T>) -> T { switch self { case .movie(let movie): return movie[keyPath: keyPath] case .book(let book): return book[keyPath: keyPath] } } // 处理影视特有属性 subscript<T>(dynamicMember keyPath: KeyPath<FilmedSpecific, T>) -> T? { switch self { case .movie(let movie): return movie[keyPath: keyPath] default: return nil } } // 处理书籍特有属性(按需添加) subscript<T>(dynamicMember keyPath: KeyPath<WrittenSpecific, T>) -> T? { switch self { case .book(let book): return book[keyPath: keyPath] default: return nil } } } // 结构体实现不变 struct Movie: Filmed { var isbn: Int var title: String var sales: Int var actors: [String] } struct Book: Written { var isbn: Int var title: String var sales: Int var author: String }
这样title、sales等公共属性只会匹配第一个下标,actors、author等特有属性匹配对应下标,不会再出现歧义。
方法二:为特有属性下标添加泛型约束
通过泛型约束,让针对Filmed的下标只接受无法被Entertainment下标处理的属性,仅适用于特有属性类型唯一且与公共属性类型无重叠的场景:
@dynamicMemberLookup enum Media { case movie(Movie) case book(Book) subscript<T>(dynamicMember keyPath: KeyPath<Entertainment, T>) -> T { switch self { case .movie(let movie): return movie[keyPath: keyPath] case .book(let book): return book[keyPath: keyPath] } } // 仅处理Filmed特有的字符串集合类型属性 subscript<T>(dynamicMember keyPath: KeyPath<Filmed, T>) -> T? where T: Collection, T.Element == String { switch self { case .movie(let movie): return movie[keyPath: keyPath] default: return nil } } }
内容的提问来源于stack exchange,提问作者Vishal Dubey
相关产品推荐
相关产品推荐

