Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(上)

发布于:2025-06-08 ⋅ 阅读:(21) ⋅ 点赞:(0)

在这里插入图片描述

概述

在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。

在这里插入图片描述

不过,在涉及到多个子类派生于基类进行多态模拟的场景下,稍不留神可能就会产生恢诡谲怪的错误。这是怎么回事?又该如何解决呢?

在本篇博文中,您将学到如下内容:

在学完本课后,相信小伙伴们都会在撸码实战中重新找回自信,并向更深一层的内功修为奋勇前进!

那还等什么呢?让我们马上开始 Swift 精进之旅吧!
Let’s go!!!😉


1. 完美世界崩塌了!

在之前的这两篇博文里:

我们已经详细讨论过了,如何借助于精心设计的 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 & AchievementEvaluator

    static 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 基类 + 子类多态场景却意外翻车的故事,随后我们深入讨论了问题的根源之所在。

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


网站公告

今日签到

点亮在社区的每一天
去签到