当前位置: 首页 > news >正文

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 类型系统的冲突导致的:

  1. Core Data 继承机制的特性
    Core Data 的实体继承在底层数据库中默认采用 单表继承 策略,所有子类实例都存储在基类对应的表中。当我们执行 Evaluator.fetchRequest() 时,实际上会查询基类 Achievement 的所有子类实例,导致返回数组中混合了不同子类的类型;
  2. 协议扩展的类型欺骗
    协议扩展中 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 基类 + 子类多态场景却意外翻车的故事,随后我们深入讨论了问题的根源之所在。

感谢观赏,我们下一篇再见吧!😎

相关文章:

  • pycharm 中文字体报错
  • PyCharm集成Conda环境
  • Doris 与 Elasticsearch:谁更适合你的数据分析需求?
  • 【Java学习笔记】String类(重点)
  • 从零开始搭建 Pytest 测试框架(Python 3.8 + PyCharm 版)
  • Android Studio 解决首次安装时下载 Gradle 慢问题
  • 【数据分析】探索婴儿年龄变化对微生物群落(呼吸道病毒和细菌病原体)结构的影响
  • 使用 C/C++ 和 OpenCV 提取图像的感兴趣区域 (ROI)
  • 游戏(game)
  • Mybatis-Plus的LambdaWrapper
  • Python_day47
  • 华为云Flexus+DeepSeek征文 | 从零到一:用Flexus云服务打造低延迟联网搜索Agent
  • Python入门手册:异常处理
  • 微前端 - Native Federation使用完整示例
  • JavaWeb的一些基础技术
  • Jenkins持续集成CI,持续部署CD,Allure报告集成以及发送电子 邮件
  • 前沿论文汇总(机器学习/深度学习/大模型/搜广推/自然语言处理)
  • EPPLUS——CAD c#读写EXCEL的第三方库
  • wpf ListBox 去除item 单击样式
  • Docker容器部署elasticsearch8.*与Kibana8.*版本使用filebeat采集日志
  • 电商网站建设报价单/软文推广经典案例
  • site网站连通率0%怎么解决/今日时政新闻热点
  • 小学生做网站/软文广告有哪些
  • 做网络平台的网站有哪些/武汉seo网站排名优化公司
  • 长沙正规企业网站制作平台/搜索引擎营销的模式有哪些
  • 哪里能找到免费网站/百度小说排行榜前十名