Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(上)
概述
在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。
不过,在涉及到多个子类派生于基类进行多态模拟的场景下,稍不留神可能就会产生恢诡谲怪的错误。这是怎么回事?又该如何解决呢?
在本篇博文中,您将学到如下内容:
- 概述
- 1. 完美世界崩塌了!
- 2. 刨根问底:问题根源之所在
- 总结
在学完本课后,相信小伙伴们都会在撸码实战中重新找回自信,并向更深一层的内功修为奋勇前进!
那还等什么呢?让我们马上开始 Swift 精进之旅吧!
Let’s go!!!😉
1. 完美世界崩塌了!
在之前的这两篇博文里:
- “一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(上)
- “一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(下)
我们已经详细讨论过了,如何借助于精心设计的 Fetchable 约束协议成功摆脱 Swift 协议扩展中的“磨搅讹绷”。
其中,我们通过一步一步完善和重构代码,解决了 Swift 语言中颇为棘手的协议关联类型系统的匹配问题。
本文后续的讨论都将建立在上面两篇博文的故事和源代码之上,如果小伙伴们在接下来的旅程中有些 “云天雾地”,请移步上述博文一探究竟。
让我们先帮助大家做一番简单的回忆,下面就是 App 中原有的 CoreData 数据库结构:Achievement 是成就基类,而 Achv_NoBreakVictory 作为成就实体类型派生于它:
@objc(Achievement)
public class Achievement: NSManagedObject {}@objc(Achv_NoBreakVictory)
public class Achv_NoBreakVictory: Achievement {}
现在,我们需要为这一成就体系增加新的成就实体类型 Achv_MultipleSerialVictories:
@objc(Achv_MultipleSerialVictories)
public class Achv_MultipleSerialVictories: Achievement {}
在如法炮制让 Achv_MultipleSerialVictories 遵守 AchievementEvaluator 协议,并实现了所有相关方法之后,编译并运行代码我们会“惊恐”地发现 App “可耻的”崩溃了,提示如下:
Fatal error: NSArray element failed to match the Swift Array Element type
Expected Achv_MultipleSerialVictories but found Achv_NoBreakVictory
在 Xcode 调试器中可以看到,此崩溃发生的位置并不在一个“正经”的地方,搞得我们有些云里雾里,非常被动:
那么,到底是 App 中哪几行代码要作为“罪魁祸首”,对此负责呢?
2. 刨根问底:问题根源之所在
为了找到问题的真正根源,我们需要再展示几小段代码,以补全缺失的拼图:
protocol Fetchable: Achievement {}extension Fetchable {static func fetchRequest() -> NSFetchRequest<Self> {// 手动构建请求,确保类型安全return NSFetchRequest<Self>(entityName: "\(Self.self)")}
}protocol AchievementEvaluator: Fetchable {associatedtype Evaluator: Fetchable & AchievementEvaluatorstatic func spawnAll(context: NSManagedObjectContext) throws
}extension AchievementEvaluator where Evaluator: Fetchable {static func calcCount(context: NSManagedObjectContext) throws -> Int {let req = Evaluator.fetchRequest()return try context.count(for: req)}static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {let req = Evaluator.fetchRequest()req.sortDescriptors = [.init(keyPath: \Achievement.orderNumber, ascending: true)]return try context.fetch(req) as! [Evaluator]}
}extension Achievement {static func spawnIfNeed(context: NSManagedObjectContext) throws {try Achv_NoBreakVictory.spawnAll(context: context)try Achv_MultipleSerialVictories.spawnAll(context: context)}
}
在上面的代码中,我们主要做了这样几件事:
- 用 Fetchable 协议作为 AchievementEvaluator 协议的约束;
- 在 Fetchable 协议扩展中创建 fetchRequest() 方法以确保类型安全;
- 在 AchievementEvaluator 协议扩展中创建了 calcCount() 和 queryAll() 方法,分别用来计算实体成就类型中实例的数量和查询实例的集合;
- 在 Achievement 成就基类中创建 spawnIfNeed() 方法用来生成所有成就实体类的实例对象;
那么,问题究竟是出在哪里呢?
原来,App 的崩溃是由于 Core Data 实体继承模型与 Swift 类型系统的冲突导致的:
- Core Data 继承机制的特性
Core Data 的实体继承在底层数据库中默认采用 单表继承 策略,所有子类实例都存储在基类对应的表中。当我们执行Evaluator.fetchRequest()
时,实际上会查询基类Achievement
的所有子类实例,导致返回数组中混合了不同子类的类型; - 协议扩展的类型欺骗
协议扩展中queryAll()
的Evaluator.fetchRequest()
虽然表面上是针对子类(如Achv_NoBreakVictory
),但实际生成的 SQL 查询却是SELECT * FROM Achievement
,返回的数组元素实际类型是基类Achievement
,强制转换为错误的子类类型时必将触发崩溃;
但是先等等,我们不是已经在 Fetchable 协议扩展中的 fetchRequest() 方法里明确说明了必须按实际的子类名称来查询的吗:
extension Fetchable {static func fetchRequest() -> NSFetchRequest<Self> {// 手动构建请求,确保类型安全return NSFetchRequest<Self>(entityName: "\(Self.self)")}
}
这个疑问不难解答。
我们在上面 fetchRequest() 方法内插入断点,再次运行可以验证:fetchRequest() 方法压根就没有执行!这说明 AchievementEvaluator 协议扩展两个方法中 Evaluator.fetchRequest()
调用的根本不是 Fetchable 协议扩展中的方法,而是托管基类 Achievement 中的默认方法!
所以,这就是问题的根本原因:我们尝试对 Achievement.fetchRequest()
方法查询出来的多种成就实体类型的实例强行做类型转换,结果可想而知。
在仅有一个实体子类时这不会产生任何问题,但当我们的 Achievement 基类派生出多个成就子类时,这个潜伏着的“致命魔鬼”就会被释放出来“为祸人间”。
那么,我们此时又该何去何从呢?
在下一篇博文中,我们将继续 Swift 精进大冒险,给出两种迥然不同的解决之道,不见不散!
总结
在本篇博文中,我们讲述了利用 Swift 协议扩展试图搞定 CoreData 基类 + 子类多态场景却意外翻车的故事,随后我们深入讨论了问题的根源之所在。
感谢观赏,我们下一篇再见吧!😎